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            #[cfg(feature = "python")]
976            JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field)?),
977            _ => Ok(JValue::Undefined),
978        },
979
980        CompiledExpr::NestedFieldLookup(outer, inner) => match data {
981            JValue::Object(obj) => {
982                // Shape-accelerated outer lookup
983                let outer_val = if let Some(shape) = shape {
984                    if let Some(&idx) = shape.get(outer.as_str()) {
985                        obj.get_index(idx).map(|(_, v)| v)
986                    } else {
987                        obj.get(outer.as_str())
988                    }
989                } else {
990                    obj.get(outer.as_str())
991                };
992                match outer_val {
993                    Some(JValue::Object(nested)) => Ok(nested
994                        .get(inner.as_str())
995                        .cloned()
996                        .unwrap_or(JValue::Undefined)),
997                    #[cfg(feature = "python")]
998                    Some(JValue::LazyPyDict(nested)) => Ok(nested.get_field(inner.as_str())?),
999                    _ => Ok(JValue::Undefined),
1000                }
1001            }
1002            #[cfg(feature = "python")]
1003            JValue::LazyPyDict(lazy) => {
1004                let outer_val = lazy.get_field(outer.as_str())?;
1005                match outer_val {
1006                    JValue::Object(nested) => Ok(nested
1007                        .get(inner.as_str())
1008                        .cloned()
1009                        .unwrap_or(JValue::Undefined)),
1010                    JValue::LazyPyDict(nested) => Ok(nested.get_field(inner.as_str())?),
1011                    _ => Ok(JValue::Undefined),
1012                }
1013            }
1014            _ => Ok(JValue::Undefined),
1015        },
1016
1017        CompiledExpr::VariableLookup(var) => {
1018            if let Some(vars) = vars {
1019                if let Some(val) = vars.get(var.as_str()) {
1020                    return Ok((*val).clone());
1021                }
1022            }
1023            // $ (empty var name) refers to the current data
1024            if var.is_empty() {
1025                return Ok(data.clone());
1026            }
1027            Ok(JValue::Undefined)
1028        }
1029
1030        // ── Comparison ──────────────────────────────────────────────────
1031        CompiledExpr::Compare { op, lhs, rhs } => {
1032            let lhs_explicit_null = is_compiled_explicit_null(lhs);
1033            let rhs_explicit_null = is_compiled_explicit_null(rhs);
1034            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1035            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1036            match op {
1037                // compiled_equal normalizes lazy operands (guarded, zero-cost when
1038                // neither side is lazy) so conversion failures raise instead of
1039                // silently comparing unequal.
1040                CompiledCmp::Eq => compiled_equal(&left, &right),
1041                CompiledCmp::Ne => match compiled_equal(&left, &right)? {
1042                    JValue::Bool(b) => Ok(JValue::Bool(!b)),
1043                    other => Ok(other),
1044                },
1045                CompiledCmp::Lt => compiled_ordered_cmp(
1046                    &left,
1047                    &right,
1048                    lhs_explicit_null,
1049                    rhs_explicit_null,
1050                    |a, b| a < b,
1051                    |a, b| a < b,
1052                ),
1053                CompiledCmp::Le => compiled_ordered_cmp(
1054                    &left,
1055                    &right,
1056                    lhs_explicit_null,
1057                    rhs_explicit_null,
1058                    |a, b| a <= b,
1059                    |a, b| a <= b,
1060                ),
1061                CompiledCmp::Gt => compiled_ordered_cmp(
1062                    &left,
1063                    &right,
1064                    lhs_explicit_null,
1065                    rhs_explicit_null,
1066                    |a, b| a > b,
1067                    |a, b| a > b,
1068                ),
1069                CompiledCmp::Ge => compiled_ordered_cmp(
1070                    &left,
1071                    &right,
1072                    lhs_explicit_null,
1073                    rhs_explicit_null,
1074                    |a, b| a >= b,
1075                    |a, b| a >= b,
1076                ),
1077            }
1078        }
1079
1080        // ── Arithmetic ──────────────────────────────────────────────────
1081        CompiledExpr::Arithmetic { op, lhs, rhs } => {
1082            let lhs_explicit_null = is_compiled_explicit_null(lhs);
1083            let rhs_explicit_null = is_compiled_explicit_null(rhs);
1084            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1085            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1086            compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
1087        }
1088
1089        // ── String concat ───────────────────────────────────────────────
1090        CompiledExpr::Concat(lhs, rhs) => {
1091            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1092            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1093            let ls = compiled_to_concat_string(&left)?;
1094            let rs = compiled_to_concat_string(&right)?;
1095            Ok(JValue::string(format!("{}{}", ls, rs)))
1096        }
1097
1098        // ── Logical ─────────────────────────────────────────────────────
1099        CompiledExpr::And(lhs, rhs) => {
1100            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1101            if !compiled_is_truthy(&left) {
1102                return Ok(JValue::Bool(false));
1103            }
1104            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1105            Ok(JValue::Bool(compiled_is_truthy(&right)))
1106        }
1107        CompiledExpr::Or(lhs, rhs) => {
1108            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1109            if compiled_is_truthy(&left) {
1110                return Ok(JValue::Bool(true));
1111            }
1112            let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1113            Ok(JValue::Bool(compiled_is_truthy(&right)))
1114        }
1115        CompiledExpr::Not(inner) => {
1116            let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1117            Ok(JValue::Bool(!compiled_is_truthy(&val)))
1118        }
1119        CompiledExpr::Negate(inner) => {
1120            let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1121            match val {
1122                JValue::Number(n) => Ok(JValue::Number(-n)),
1123                JValue::Null => Ok(JValue::Null),
1124                // Undefined operand propagates through unary minus, matching the tree-walker.
1125                v if v.is_undefined() => Ok(JValue::Undefined),
1126                _ => Err(EvaluatorError::TypeError(
1127                    "D1002: Cannot negate non-number value".to_string(),
1128                )),
1129            }
1130        }
1131
1132        // ── Conditional ─────────────────────────────────────────────────
1133        CompiledExpr::Conditional {
1134            condition,
1135            then_expr,
1136            else_expr,
1137        } => {
1138            let cond = eval_compiled_inner(condition, data, vars, ctx, shape, options, start_time)?;
1139            if compiled_is_truthy(&cond) {
1140                eval_compiled_inner(then_expr, data, vars, ctx, shape, options, start_time)
1141            } else if let Some(else_e) = else_expr {
1142                eval_compiled_inner(else_e, data, vars, ctx, shape, options, start_time)
1143            } else {
1144                Ok(JValue::Undefined)
1145            }
1146        }
1147
1148        // ── Object construction ─────────────────────────────────────────
1149        CompiledExpr::ObjectConstruct(fields) => {
1150            let mut result = IndexMap::with_capacity(fields.len());
1151            for (key, expr) in fields {
1152                let value = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1153                if !value.is_undefined() {
1154                    result.insert(key.clone(), value);
1155                }
1156            }
1157            Ok(JValue::object(result))
1158        }
1159
1160        // ── Array construction ──────────────────────────────────────────
1161        CompiledExpr::ArrayConstruct(elems) => {
1162            let mut result = Vec::new();
1163            for (elem_expr, is_nested) in elems {
1164                let value =
1165                    eval_compiled_inner(elem_expr, data, vars, ctx, shape, options, start_time)?;
1166                // Undefined values are excluded from array constructors (tree-walker parity)
1167                if value.is_undefined() {
1168                    continue;
1169                }
1170                if *is_nested {
1171                    // Explicit array constructor [...] — keep nested even if it's an array
1172                    result.push(value);
1173                } else if let JValue::Array(arr) = value {
1174                    // Non-constructor that evaluated to an array — flatten one level
1175                    result.extend(arr.iter().cloned());
1176                } else {
1177                    result.push(value);
1178                }
1179            }
1180            Ok(JValue::array(result))
1181        }
1182
1183        // ── Phase 2 new variants ─────────────────────────────────────────
1184
1185        // ContextVar: named variable lookup from context scope.
1186        // In top-level mode (ctx=None, no bindings), returns Undefined.
1187        // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
1188        // is only ever populated for top-level calls — always Undefined there.
1189        CompiledExpr::ContextVar(name) => {
1190            // Check vars map first (for lambda params that might shadow context)
1191            if let Some(vars) = vars {
1192                if let Some(val) = vars.get(name.as_str()) {
1193                    return Ok((*val).clone());
1194                }
1195            }
1196            // Then check context scope
1197            if let Some(ctx) = ctx {
1198                if let Some(val) = ctx.lookup(name) {
1199                    return Ok(val.clone());
1200                }
1201            }
1202            Ok(JValue::Undefined)
1203        }
1204
1205        // FieldPath: multi-step field access with implicit array mapping.
1206        CompiledExpr::FieldPath(steps) => {
1207            compiled_eval_field_path(steps, data, vars, ctx, shape, options, start_time)
1208        }
1209
1210        // BuiltinCall: evaluate all args, dispatch to pure builtin.
1211        CompiledExpr::BuiltinCall { name, args } => {
1212            let mut evaled_args = Vec::with_capacity(args.len());
1213            for arg in args.iter() {
1214                evaled_args.push(eval_compiled_inner(
1215                    arg, data, vars, ctx, shape, options, start_time,
1216                )?);
1217            }
1218            call_pure_builtin(name, &evaled_args, data, options)
1219        }
1220
1221        // Block: evaluate each expression in sequence, return the last value.
1222        CompiledExpr::Block(exprs) => {
1223            let mut result = JValue::Undefined;
1224            for expr in exprs.iter() {
1225                result = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1226            }
1227            Ok(result)
1228        }
1229
1230        // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1231        // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1232        CompiledExpr::Coalesce(lhs, rhs) => {
1233            let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1234            if left.is_undefined() {
1235                eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)
1236            } else {
1237                Ok(left)
1238            }
1239        }
1240
1241        // ── Higher-order functions ─────────────────────────────────────────────
1242        //
1243        // These variants are emitted by try_compile_hof_expr when the HOF argument
1244        // is an inline lambda literal with a compilable body. Outer vars are merged
1245        // with the lambda params so that nested HOF can access variables from
1246        // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1247        CompiledExpr::MapCall {
1248            array,
1249            params,
1250            body,
1251        } => {
1252            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1253            let single_holder;
1254            let items: &[JValue] = match &arr_val {
1255                JValue::Array(a) => a.as_slice(),
1256                JValue::Undefined => return Ok(JValue::Undefined),
1257                other => {
1258                    single_holder = [other.clone()];
1259                    &single_holder[..]
1260                }
1261            };
1262            let mut result = Vec::with_capacity(items.len());
1263            let p0 = params.first().map(|s| s.as_str());
1264
1265            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1266                // 2-param lambda (element + index): build per-iteration because idx_val
1267                // is loop-local and cannot outlive the iteration.
1268                for (idx, item) in items.iter().enumerate() {
1269                    check_loop_timeout(options, start_time)?;
1270                    let idx_val = JValue::Number(idx as f64);
1271                    let mut call_vars = clone_outer_vars(vars, 2);
1272                    if let Some(p) = p0 {
1273                        call_vars.insert(p, item);
1274                    }
1275                    call_vars.insert(p1, &idx_val);
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            } else if let Some(p0) = p0 {
1290                // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1291                let mut call_vars = clone_outer_vars(vars, 1);
1292                for item in items.iter() {
1293                    check_loop_timeout(options, start_time)?;
1294                    call_vars.insert(p0, item);
1295                    let mapped = eval_compiled_inner(
1296                        body,
1297                        data,
1298                        Some(&call_vars),
1299                        ctx,
1300                        shape,
1301                        options,
1302                        start_time,
1303                    )?;
1304                    if !mapped.is_undefined() {
1305                        result.push(mapped);
1306                    }
1307                }
1308            }
1309            check_sequence_length(result.len(), options)?;
1310            Ok(if result.is_empty() {
1311                JValue::Undefined
1312            } else {
1313                JValue::array(result)
1314            })
1315        }
1316
1317        CompiledExpr::FilterCall {
1318            array,
1319            params,
1320            body,
1321        } => {
1322            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1323            if arr_val.is_undefined() || arr_val.is_null() {
1324                return Ok(JValue::Undefined);
1325            }
1326            let single_holder;
1327            let (items, was_single) = match &arr_val {
1328                JValue::Array(a) => (a.as_slice(), false),
1329                other => {
1330                    single_holder = [other.clone()];
1331                    (&single_holder[..], true)
1332                }
1333            };
1334            let mut result = Vec::with_capacity(items.len() / 2);
1335            let p0 = params.first().map(|s| s.as_str());
1336
1337            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1338                for (idx, item) in items.iter().enumerate() {
1339                    check_loop_timeout(options, start_time)?;
1340                    let idx_val = JValue::Number(idx as f64);
1341                    let mut call_vars = clone_outer_vars(vars, 2);
1342                    if let Some(p) = p0 {
1343                        call_vars.insert(p, item);
1344                    }
1345                    call_vars.insert(p1, &idx_val);
1346                    let pred = eval_compiled_inner(
1347                        body,
1348                        data,
1349                        Some(&call_vars),
1350                        ctx,
1351                        shape,
1352                        options,
1353                        start_time,
1354                    )?;
1355                    if compiled_is_truthy(&pred) {
1356                        result.push(item.clone());
1357                    }
1358                }
1359            } else if let Some(p0) = p0 {
1360                let mut call_vars = clone_outer_vars(vars, 1);
1361                for item in items.iter() {
1362                    check_loop_timeout(options, start_time)?;
1363                    call_vars.insert(p0, item);
1364                    let pred = eval_compiled_inner(
1365                        body,
1366                        data,
1367                        Some(&call_vars),
1368                        ctx,
1369                        shape,
1370                        options,
1371                        start_time,
1372                    )?;
1373                    if compiled_is_truthy(&pred) {
1374                        result.push(item.clone());
1375                    }
1376                }
1377            }
1378            if was_single {
1379                Ok(match result.len() {
1380                    0 => JValue::Undefined,
1381                    1 => {
1382                        check_sequence_length(1, options)?;
1383                        result.remove(0)
1384                    }
1385                    _ => {
1386                        check_sequence_length(result.len(), options)?;
1387                        JValue::array(result)
1388                    }
1389                })
1390            } else {
1391                check_sequence_length(result.len(), options)?;
1392                Ok(JValue::array(result))
1393            }
1394        }
1395
1396        CompiledExpr::ReduceCall {
1397            array,
1398            params,
1399            body,
1400            initial,
1401        } => {
1402            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1403            let single_holder;
1404            let items: &[JValue] = match &arr_val {
1405                JValue::Array(a) => a.as_slice(),
1406                JValue::Null => return Ok(JValue::Null),
1407                JValue::Undefined => return Ok(JValue::Undefined),
1408                other => {
1409                    single_holder = [other.clone()];
1410                    &single_holder[..]
1411                }
1412            };
1413            let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1414                let init_val =
1415                    eval_compiled_inner(init_expr, data, vars, ctx, shape, options, start_time)?;
1416                if items.is_empty() {
1417                    return Ok(init_val);
1418                }
1419                (0usize, init_val)
1420            } else {
1421                if items.is_empty() {
1422                    return Ok(JValue::Null);
1423                }
1424                (1, items[0].clone())
1425            };
1426            let acc_param = params[0].as_str();
1427            let item_param = params[1].as_str();
1428            for item in items[start_idx..].iter() {
1429                check_loop_timeout(options, start_time)?;
1430                // Per-iteration HashMap: &accumulator borrow must be released before we
1431                // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1432                let mut call_vars = clone_outer_vars(vars, 2);
1433                call_vars.insert(acc_param, &accumulator);
1434                call_vars.insert(item_param, item);
1435                let new_acc = eval_compiled_inner(
1436                    body,
1437                    data,
1438                    Some(&call_vars),
1439                    ctx,
1440                    shape,
1441                    options,
1442                    start_time,
1443                )?;
1444                drop(call_vars);
1445                accumulator = new_acc;
1446            }
1447            Ok(accumulator)
1448        }
1449    }
1450}
1451
1452/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1453#[inline]
1454pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1455    match value {
1456        JValue::Null | JValue::Undefined => false,
1457        JValue::Bool(b) => *b,
1458        JValue::Number(n) => *n != 0.0,
1459        JValue::String(s) => !s.is_empty(),
1460        JValue::Array(a) => !a.is_empty(),
1461        JValue::Object(o) => !o.is_empty(),
1462        _ => false,
1463    }
1464}
1465
1466/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1467/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1468#[inline]
1469fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1470    matches!(expr, CompiledExpr::ExplicitNull)
1471}
1472
1473/// Ordered comparison for compiled expressions.
1474/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1475#[inline]
1476pub(crate) fn compiled_ordered_cmp(
1477    left: &JValue,
1478    right: &JValue,
1479    left_is_explicit_null: bool,
1480    right_is_explicit_null: bool,
1481    cmp_num: fn(f64, f64) -> bool,
1482    cmp_str: fn(&str, &str) -> bool,
1483) -> Result<JValue, EvaluatorError> {
1484    match (left, right) {
1485        (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1486        (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1487        // Both null/undefined → undefined
1488        (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1489        (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1490        // Explicit null literal with any non-null type → T2010 error
1491        (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1492            "T2010: Type mismatch in comparison".to_string(),
1493        )),
1494        (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1495            "T2010: Type mismatch in comparison".to_string(),
1496        )),
1497        // Boolean with undefined → T2010 error
1498        (JValue::Bool(_), JValue::Null | JValue::Undefined)
1499        | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1500            EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1501        ),
1502        // Number or String with implicit undefined (missing field) → undefined result
1503        (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1504        | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1505            Ok(JValue::Null)
1506        }
1507        // Type mismatch (string vs number)
1508        (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1509            Err(EvaluatorError::EvaluationError(
1510                "T2009: The expressions on either side of operator must be of the same data type"
1511                    .to_string(),
1512            ))
1513        }
1514        _ => Err(EvaluatorError::EvaluationError(
1515            "T2010: Type mismatch in comparison".to_string(),
1516        )),
1517    }
1518}
1519
1520/// Arithmetic for compiled expressions.
1521/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1522#[inline]
1523pub(crate) fn compiled_arithmetic(
1524    op: CompiledArithOp,
1525    left: &JValue,
1526    right: &JValue,
1527    left_is_explicit_null: bool,
1528    right_is_explicit_null: bool,
1529) -> Result<JValue, EvaluatorError> {
1530    let op_sym = match op {
1531        CompiledArithOp::Add => "+",
1532        CompiledArithOp::Sub => "-",
1533        CompiledArithOp::Mul => "*",
1534        CompiledArithOp::Div => "/",
1535        CompiledArithOp::Mod => "%",
1536    };
1537    match (left, right) {
1538        (JValue::Number(a), JValue::Number(b)) => {
1539            let result = match op {
1540                CompiledArithOp::Add => *a + *b,
1541                CompiledArithOp::Sub => *a - *b,
1542                CompiledArithOp::Mul => {
1543                    let r = *a * *b;
1544                    if r.is_infinite() {
1545                        return Err(EvaluatorError::EvaluationError(
1546                            "D1001: Number out of range".to_string(),
1547                        ));
1548                    }
1549                    r
1550                }
1551                CompiledArithOp::Div => {
1552                    if *b == 0.0 {
1553                        return Err(EvaluatorError::EvaluationError(
1554                            "Division by zero".to_string(),
1555                        ));
1556                    }
1557                    *a / *b
1558                }
1559                CompiledArithOp::Mod => {
1560                    if *b == 0.0 {
1561                        return Err(EvaluatorError::EvaluationError(
1562                            "Division by zero".to_string(),
1563                        ));
1564                    }
1565                    *a % *b
1566                }
1567            };
1568            Ok(JValue::Number(result))
1569        }
1570        // Explicit null literal → T2002 error (matching tree-walker behavior)
1571        (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1572            Err(EvaluatorError::TypeError(format!(
1573                "T2002: The left side of the {} operator must evaluate to a number",
1574                op_sym
1575            )))
1576        }
1577        (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1578            Err(EvaluatorError::TypeError(format!(
1579                "T2002: The right side of the {} operator must evaluate to a number",
1580                op_sym
1581            )))
1582        }
1583        // Implicit undefined propagation (from missing field) → undefined result
1584        (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1585            Ok(JValue::Null)
1586        }
1587        _ => Err(EvaluatorError::TypeError(format!(
1588            "Cannot apply {} to {:?} and {:?}",
1589            op_sym, left, right
1590        ))),
1591    }
1592}
1593
1594/// Convert a value to string for concatenation in compiled expressions.
1595#[inline]
1596pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1597    // Normalize a lazy operand up front: `functions::string::string`'s lazy arm maps a
1598    // conversion failure to `JValue::Null` (silently stringifying to `""`), which would
1599    // swallow the TypeError this must raise instead. Guarded by `is_lazy` so the common
1600    // (non-lazy) path pays no clone.
1601    let normalized;
1602    let value = if value.is_lazy() {
1603        normalized = normalize_lazy(value)?;
1604        &normalized
1605    } else {
1606        value
1607    };
1608    match value {
1609        JValue::String(s) => Ok(s.to_string()),
1610        JValue::Null | JValue::Undefined => Ok(String::new()),
1611        JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1612            match crate::functions::string::string(value, None) {
1613                Ok(JValue::String(s)) => Ok(s.to_string()),
1614                Ok(JValue::Null) => Ok(String::new()),
1615                _ => Err(EvaluatorError::TypeError(
1616                    "Cannot concatenate complex types".to_string(),
1617                )),
1618            }
1619        }
1620        _ => Ok(String::new()),
1621    }
1622}
1623
1624/// Equality comparison for the bytecode VM.
1625#[inline]
1626pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> Result<JValue, EvaluatorError> {
1627    // Normalize lazy operands so a conversion failure raises TypeError here rather than
1628    // being swallowed as `false` by `values_equal`'s lazy `to_object_ref` arms. Guarded
1629    // by `is_lazy` so the common (non-lazy) path pays no clone.
1630    if lhs.is_lazy() || rhs.is_lazy() {
1631        let lhs = normalize_lazy(lhs)?;
1632        let rhs = normalize_lazy(rhs)?;
1633        return Ok(JValue::Bool(crate::functions::array::values_equal(
1634            &lhs, &rhs,
1635        )));
1636    }
1637    Ok(JValue::Bool(crate::functions::array::values_equal(
1638        lhs, rhs,
1639    )))
1640}
1641
1642/// String concatenation for the bytecode VM.
1643#[inline]
1644pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1645    let l = compiled_to_concat_string(&lhs)?;
1646    let r = compiled_to_concat_string(&rhs)?;
1647    Ok(JValue::string(l + &r))
1648}
1649
1650/// Entry point for the bytecode VM to call pure builtins by name.
1651#[inline]
1652pub(crate) fn call_pure_builtin_by_name(
1653    name: &str,
1654    args: &[JValue],
1655    data: &JValue,
1656    options: &EvaluatorOptions,
1657) -> Result<JValue, EvaluatorError> {
1658    call_pure_builtin(name, args, data, options)
1659}
1660
1661// ──────────────────────────────────────────────────────────────────────────────
1662// Phase 2: path compilation, builtin dispatch, and supporting helpers
1663// ──────────────────────────────────────────────────────────────────────────────
1664
1665/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1666///
1667/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1668/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1669fn try_compile_path(
1670    steps: &[crate::ast::PathStep],
1671    allowed_vars: Option<&[&str]>,
1672) -> Option<CompiledExpr> {
1673    use crate::ast::{AstNode, Stage};
1674
1675    if steps.is_empty() {
1676        return None;
1677    }
1678
1679    // Determine the start of the path:
1680    //   `$.field...`  → starts from current data (drop the leading `$` step)
1681    //   `$var.field`  → variable-prefixed paths: not compiled yet, fall back to tree-walker
1682    //   `field...`    → starts from current data
1683    let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1684        AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1685        AstNode::Variable(_) => return None,
1686        AstNode::Name(_) => steps,
1687        _ => return None,
1688    };
1689
1690    // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1691    // which represent index access in JSONata, not boolean filtering, and the
1692    // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1693    // to stay an array rather than filtering — the tree-walker's
1694    // `evaluate_predicate` special-cases it and the compiled path has no
1695    // equivalent, so bail out rather than silently treating it as `filter(true)`.
1696    let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1697        if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1698            return None;
1699        }
1700        try_compile_expr_inner(node, allowed_vars)
1701    };
1702
1703    // Compile each field step.
1704    // Handles:
1705    //   - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1706    //   - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1707    //     previous step's filter slot, since both encodings have identical runtime semantics.
1708    let mut compiled_steps = Vec::with_capacity(field_steps.len());
1709    for step in field_steps {
1710        // Tuple-stream steps (@ focus / # index / % parent binding) require the
1711        // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1712        // tuple handling). Never compile them to the flat bytecode field path,
1713        // which is unaware of the binding flags and would silently drop them.
1714        if step.focus.is_some()
1715            || step.index_var.is_some()
1716            || step.ancestor_label.is_some()
1717            || step.is_tuple
1718        {
1719            return None;
1720        }
1721        match &step.node {
1722            AstNode::Name(name) => {
1723                let filter = match step.stages.as_slice() {
1724                    [] => None,
1725                    [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1726                    _ => return None,
1727                };
1728                compiled_steps.push(CompiledStep {
1729                    field: name.clone(),
1730                    filter,
1731                });
1732            }
1733            AstNode::Predicate(filter_node) => {
1734                // Standalone predicate step — fold into the previous Name step's filter slot.
1735                if !step.stages.is_empty() {
1736                    return None;
1737                }
1738                let last = compiled_steps.last_mut()?;
1739                if last.filter.is_some() {
1740                    return None;
1741                }
1742                last.filter = Some(compile_filter(filter_node)?);
1743            }
1744            _ => return None,
1745        }
1746    }
1747
1748    if compiled_steps.is_empty() {
1749        // Bare `$` with no further field steps — current-data reference
1750        return Some(CompiledExpr::VariableLookup(String::new()));
1751    }
1752
1753    // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1754    // in HOF mode (allowed_vars=Some), where data is always a single Object element
1755    // from an array. In top-level mode (allowed_vars=None), data can itself be an
1756    // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1757    if allowed_vars.is_some() {
1758        if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1759            return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1760        }
1761        if compiled_steps.len() == 2
1762            && compiled_steps[0].filter.is_none()
1763            && compiled_steps[1].filter.is_none()
1764        {
1765            let outer = compiled_steps.remove(0).field;
1766            let inner = compiled_steps.remove(0).field;
1767            return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1768        }
1769    }
1770
1771    Some(CompiledExpr::FieldPath(compiled_steps))
1772}
1773
1774/// Evaluate a compiled `FieldPath` against `data`.
1775///
1776/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1777/// Filters are applied as predicates: truthy elements are kept.
1778///
1779/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1780/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1781/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1782fn compiled_eval_field_path(
1783    steps: &[CompiledStep],
1784    data: &JValue,
1785    vars: Option<&HashMap<&str, &JValue>>,
1786    ctx: Option<&Context>,
1787    shape: Option<&ShapeCache>,
1788    options: &EvaluatorOptions,
1789    start_time: Option<Instant>,
1790) -> Result<JValue, EvaluatorError> {
1791    let mut current = data.clone();
1792    // Track whether the most recent field step mapped over an array (like the tree-walker's
1793    // `did_array_mapping` flag). Filters also count as array operations.
1794    let mut did_array_mapping = false;
1795    for step in steps {
1796        // Determine if this step will do array mapping before we overwrite `current`
1797        let is_array = matches!(current, JValue::Array(_));
1798        // Field access with implicit array mapping
1799        current = compiled_field_step(&step.field, &current, options)?;
1800        if is_array {
1801            did_array_mapping = true;
1802        } else {
1803            // Extracting from a single object resets the flag (tree-walker parity)
1804            did_array_mapping = false;
1805        }
1806        // Apply filter if present (filter is an array operation — keep the flag set)
1807        if let Some(filter) = &step.filter {
1808            current =
1809                compiled_apply_filter(filter, &current, vars, ctx, shape, options, start_time)?;
1810            // Filter always implies we operated on an array
1811            did_array_mapping = true;
1812        }
1813    }
1814    // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1815    if did_array_mapping {
1816        Ok(match current {
1817            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1818            other => other,
1819        })
1820    } else {
1821        Ok(current)
1822    }
1823}
1824
1825/// Perform a single-field access with implicit array-mapping semantics.
1826///
1827/// - Object: look up `field`, return its value or Undefined
1828/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1829///   (this is a query-result sequence, so D2015 applies — mirrors `evaluate_path`'s
1830///   array-mapping check and `vm.rs`'s `get_field_cached`)
1831/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1832/// - Other: Undefined
1833fn compiled_field_step(
1834    field: &str,
1835    value: &JValue,
1836    options: &EvaluatorOptions,
1837) -> Result<JValue, EvaluatorError> {
1838    match value {
1839        JValue::Object(obj) => {
1840            // Check for tuple: extract from "@" inner object
1841            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1842                match obj.get("@") {
1843                    Some(JValue::Object(inner)) => {
1844                        return Ok(inner.get(field).cloned().unwrap_or(JValue::Undefined));
1845                    }
1846                    #[cfg(feature = "python")]
1847                    Some(JValue::LazyPyDict(lazy)) => {
1848                        return Ok(lazy.get_field(field)?);
1849                    }
1850                    _ => {}
1851                }
1852                return Ok(JValue::Undefined);
1853            }
1854            Ok(obj.get(field).cloned().unwrap_or(JValue::Undefined))
1855        }
1856        #[cfg(feature = "python")]
1857        JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field)?),
1858        JValue::Array(arr) => {
1859            // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1860            let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1861                if let JValue::Object(obj) = v {
1862                    if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1863                        return build_shape_cache(v);
1864                    }
1865                }
1866                None
1867            });
1868            let mut result = Vec::new();
1869            for item in arr.iter() {
1870                let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1871                    // Tuple objects need the recursive path for "@" inner lookup.
1872                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1873                        compiled_field_step(field, item, options)?
1874                    } else if let Some(&pos) = sh.get(field) {
1875                        // Positional access with key verification: guards against heterogeneous
1876                        // schemas (objects where the same field is at a different index).
1877                        // On a mismatch, fall back to a regular hash lookup.
1878                        match obj.get_index(pos) {
1879                            Some((k, v)) if k.as_str() == field => v.clone(),
1880                            _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1881                        }
1882                    } else {
1883                        // Field not in the first object's schema — fall back to hash lookup
1884                        // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1885                        obj.get(field).cloned().unwrap_or(JValue::Undefined)
1886                    }
1887                } else {
1888                    compiled_field_step(field, item, options)?
1889                };
1890                match extracted {
1891                    JValue::Undefined => {}
1892                    JValue::Array(inner) => result.extend(inner.iter().cloned()),
1893                    other => result.push(other),
1894                }
1895            }
1896            check_sequence_length(result.len(), options)?;
1897            Ok(if result.is_empty() {
1898                JValue::Undefined
1899            } else {
1900                JValue::array(result)
1901            })
1902        }
1903        _ => Ok(JValue::Undefined),
1904    }
1905}
1906
1907/// Apply a compiled filter predicate to a value.
1908///
1909/// - Array: return elements for which the predicate is truthy
1910/// - Single value: return it if predicate is truthy, else Undefined
1911/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1912fn compiled_apply_filter(
1913    filter: &CompiledExpr,
1914    value: &JValue,
1915    vars: Option<&HashMap<&str, &JValue>>,
1916    ctx: Option<&Context>,
1917    shape: Option<&ShapeCache>,
1918    options: &EvaluatorOptions,
1919    start_time: Option<Instant>,
1920) -> Result<JValue, EvaluatorError> {
1921    match value {
1922        JValue::Array(arr) => {
1923            let mut result = Vec::new();
1924            // Auto-build shape cache from first element when not provided.
1925            // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1926            let local_shape: Option<ShapeCache> = if shape.is_none() {
1927                arr.first().and_then(build_shape_cache)
1928            } else {
1929                None
1930            };
1931            let effective_shape = shape.or(local_shape.as_ref());
1932            for item in arr.iter() {
1933                check_loop_timeout(options, start_time)?;
1934                let pred = eval_compiled_inner(
1935                    filter,
1936                    item,
1937                    vars,
1938                    ctx,
1939                    effective_shape,
1940                    options,
1941                    start_time,
1942                )?;
1943                if compiled_is_truthy(&pred) {
1944                    result.push(item.clone());
1945                }
1946            }
1947            if result.is_empty() {
1948                Ok(JValue::Undefined)
1949            } else if result.len() == 1 {
1950                check_sequence_length(1, options)?;
1951                Ok(result.remove(0))
1952            } else {
1953                check_sequence_length(result.len(), options)?;
1954                Ok(JValue::array(result))
1955            }
1956        }
1957        JValue::Undefined => Ok(JValue::Undefined),
1958        _ => {
1959            let pred = eval_compiled_inner(filter, value, vars, ctx, shape, options, start_time)?;
1960            if compiled_is_truthy(&pred) {
1961                Ok(value.clone())
1962            } else {
1963                Ok(JValue::Undefined)
1964            }
1965        }
1966    }
1967}
1968
1969/// Materialize a top-level lazy dict into a plain Object. Non-lazy values
1970/// pass through unchanged. Does NOT recurse into arrays/objects — element-
1971/// level laziness is handled by the specific consumers that need it.
1972#[cfg(feature = "python")]
1973pub(crate) fn normalize_lazy(value: &JValue) -> Result<JValue, EvaluatorError> {
1974    match value {
1975        JValue::LazyPyDict(lazy) => Ok(JValue::Object(lazy.to_object()?)),
1976        _ => Ok(value.clone()),
1977    }
1978}
1979
1980#[cfg(not(feature = "python"))]
1981pub(crate) fn normalize_lazy(value: &JValue) -> Result<JValue, EvaluatorError> {
1982    Ok(value.clone())
1983}
1984
1985/// Dispatch a pure builtin function call.
1986///
1987/// Replicates the tree-walker's evaluation for the subset of builtins in
1988/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1989/// `data` is the current context value for implicit-argument insertion.
1990fn call_pure_builtin(
1991    name: &str,
1992    args: &[JValue],
1993    data: &JValue,
1994    options: &EvaluatorOptions,
1995) -> Result<JValue, EvaluatorError> {
1996    use crate::functions;
1997
1998    // Apply implicit context insertion matching the tree-walker
1999    let args_storage: Vec<JValue>;
2000    let effective_args: &[JValue] = if args.is_empty() {
2001        match name {
2002            "string" => {
2003                // $string() with a null/undefined context returns undefined, not "null".
2004                // This mirrors the tree-walker's special case at the function-call site.
2005                if data.is_undefined() || data.is_null() {
2006                    return Ok(JValue::Undefined);
2007                }
2008                args_storage = vec![data.clone()];
2009                &args_storage
2010            }
2011            "number" | "boolean" | "uppercase" | "lowercase" => {
2012                args_storage = vec![data.clone()];
2013                &args_storage
2014            }
2015            _ => args,
2016        }
2017    } else if args.len() == 1 {
2018        match name {
2019            "substringBefore" | "substringAfter" | "contains" | "split" => {
2020                if matches!(data, JValue::String(_)) {
2021                    args_storage = std::iter::once(data.clone())
2022                        .chain(args.iter().cloned())
2023                        .collect();
2024                    &args_storage
2025                } else {
2026                    args
2027                }
2028            }
2029            _ => args,
2030        }
2031    } else {
2032        args
2033    };
2034
2035    // Materialize top-level lazy args so every builtin sees plain Objects.
2036    #[cfg(feature = "python")]
2037    let lazy_storage: Vec<JValue>;
2038    #[cfg(feature = "python")]
2039    let effective_args: &[JValue] = if effective_args
2040        .iter()
2041        .any(|a| matches!(a, JValue::LazyPyDict(_)))
2042    {
2043        lazy_storage = effective_args
2044            .iter()
2045            .map(normalize_lazy)
2046            .collect::<Result<Vec<_>, _>>()?;
2047        &lazy_storage
2048    } else {
2049        effective_args
2050    };
2051
2052    // Apply undefined propagation: if the first effective argument is Undefined
2053    // and the function propagates undefined, return Undefined immediately.
2054    // This matches the tree-walker's `propagates_undefined` check.
2055    if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
2056        return Ok(JValue::Undefined);
2057    }
2058
2059    match name {
2060        // ── String functions ────────────────────────────────────────────
2061        "string" => {
2062            // Validate the optional prettify argument: must be a boolean.
2063            let prettify = match effective_args.get(1) {
2064                None => None,
2065                Some(JValue::Bool(b)) => Some(*b),
2066                Some(_) => {
2067                    return Err(EvaluatorError::TypeError(
2068                        "string() prettify parameter must be a boolean".to_string(),
2069                    ))
2070                }
2071            };
2072            let arg = effective_args.first().unwrap_or(&JValue::Null);
2073            Ok(functions::string::string(arg, prettify)?)
2074        }
2075        "length" => match effective_args.first() {
2076            Some(JValue::String(s)) => Ok(functions::string::length(s)?),
2077            // Undefined input propagates (caught above by the undefined-propagation guard).
2078            Some(JValue::Undefined) => Ok(JValue::Undefined),
2079            // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
2080            // so the test framework accepts it against any expected T-code).
2081            None => Err(EvaluatorError::EvaluationError(
2082                "length() requires exactly 1 argument".to_string(),
2083            )),
2084            // null and any other non-string type → T0410
2085            _ => Err(EvaluatorError::TypeError(
2086                "T0410: Argument 1 of function length does not match function signature"
2087                    .to_string(),
2088            )),
2089        },
2090        "uppercase" => match effective_args.first() {
2091            Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
2092            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2093            _ => Err(EvaluatorError::TypeError(
2094                "T0410: Argument 1 of function uppercase does not match function signature"
2095                    .to_string(),
2096            )),
2097        },
2098        "lowercase" => match effective_args.first() {
2099            Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
2100            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2101            _ => Err(EvaluatorError::TypeError(
2102                "T0410: Argument 1 of function lowercase does not match function signature"
2103                    .to_string(),
2104            )),
2105        },
2106        "trim" => match effective_args.first() {
2107            None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
2108            Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
2109            _ => Err(EvaluatorError::TypeError(
2110                "trim() requires a string argument".to_string(),
2111            )),
2112        },
2113        "substring" => {
2114            if effective_args.len() < 2 {
2115                return Err(EvaluatorError::EvaluationError(
2116                    "substring() requires at least 2 arguments".to_string(),
2117                ));
2118            }
2119            match (&effective_args[0], &effective_args[1]) {
2120                (JValue::String(s), JValue::Number(start)) => {
2121                    // Optional 3rd arg (length) must be a number if provided.
2122                    let length = match effective_args.get(2) {
2123                        None => None,
2124                        Some(JValue::Number(l)) => Some(*l as i64),
2125                        Some(_) => {
2126                            return Err(EvaluatorError::TypeError(
2127                                "T0410: Argument 3 of function substring does not match function signature"
2128                                    .to_string(),
2129                            ))
2130                        }
2131                    };
2132                    Ok(functions::string::substring(s, *start as i64, length)?)
2133                }
2134                _ => Err(EvaluatorError::TypeError(
2135                    "T0410: Argument 1 of function substring does not match function signature"
2136                        .to_string(),
2137                )),
2138            }
2139        }
2140        "substringBefore" => {
2141            if effective_args.len() != 2 {
2142                return Err(EvaluatorError::TypeError(
2143                    "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
2144                ));
2145            }
2146            match (&effective_args[0], &effective_args[1]) {
2147                (JValue::String(s), JValue::String(sep)) => {
2148                    Ok(functions::string::substring_before(s, sep)?)
2149                }
2150                // Undefined propagates; null is a type error.
2151                (JValue::Undefined, _) => Ok(JValue::Undefined),
2152                _ => Err(EvaluatorError::TypeError(
2153                    "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
2154                )),
2155            }
2156        }
2157        "substringAfter" => {
2158            if effective_args.len() != 2 {
2159                return Err(EvaluatorError::TypeError(
2160                    "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
2161                ));
2162            }
2163            match (&effective_args[0], &effective_args[1]) {
2164                (JValue::String(s), JValue::String(sep)) => {
2165                    Ok(functions::string::substring_after(s, sep)?)
2166                }
2167                // Undefined propagates; null is a type error.
2168                (JValue::Undefined, _) => Ok(JValue::Undefined),
2169                _ => Err(EvaluatorError::TypeError(
2170                    "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
2171                )),
2172            }
2173        }
2174        "contains" => {
2175            if effective_args.len() != 2 {
2176                return Err(EvaluatorError::EvaluationError(
2177                    "contains() requires exactly 2 arguments".to_string(),
2178                ));
2179            }
2180            match &effective_args[0] {
2181                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2182                JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
2183                _ => Err(EvaluatorError::TypeError(
2184                    "contains() requires a string as the first argument".to_string(),
2185                )),
2186            }
2187        }
2188        "split" => {
2189            if effective_args.len() < 2 {
2190                return Err(EvaluatorError::EvaluationError(
2191                    "split() requires at least 2 arguments".to_string(),
2192                ));
2193            }
2194            match &effective_args[0] {
2195                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2196                JValue::String(s) => {
2197                    // Validate the optional limit argument — must be a positive number.
2198                    let limit = match effective_args.get(2) {
2199                        None => None,
2200                        Some(JValue::Number(n)) => {
2201                            if *n < 0.0 {
2202                                return Err(EvaluatorError::EvaluationError(
2203                                    "D3020: Third argument of split function must be a positive number"
2204                                        .to_string(),
2205                                ));
2206                            }
2207                            Some(n.floor() as usize)
2208                        }
2209                        Some(_) => {
2210                            return Err(EvaluatorError::TypeError(
2211                                "split() limit must be a number".to_string(),
2212                            ))
2213                        }
2214                    };
2215                    Ok(functions::string::split(s, &effective_args[1], limit)?)
2216                }
2217                _ => Err(EvaluatorError::TypeError(
2218                    "split() requires a string as the first argument".to_string(),
2219                )),
2220            }
2221        }
2222        "join" => {
2223            if effective_args.is_empty() {
2224                return Err(EvaluatorError::TypeError(
2225                    "T0410: Argument 1 of function $join does not match function signature"
2226                        .to_string(),
2227                ));
2228            }
2229            match &effective_args[0] {
2230                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2231                // Signature: <a<s>s?:s> — first arg must be an array of strings.
2232                JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
2233                    Err(EvaluatorError::TypeError(
2234                        "T0412: Argument 1 of function $join must be an array of String"
2235                            .to_string(),
2236                    ))
2237                }
2238                #[cfg(feature = "python")]
2239                JValue::LazyPyDict(_) => Err(EvaluatorError::TypeError(
2240                    "T0412: Argument 1 of function $join must be an array of String".to_string(),
2241                )),
2242                JValue::Array(arr) => {
2243                    // All elements must be strings.
2244                    for item in arr.iter() {
2245                        if !matches!(item, JValue::String(_)) {
2246                            return Err(EvaluatorError::TypeError(
2247                                "T0412: Argument 1 of function $join must be an array of String"
2248                                    .to_string(),
2249                            ));
2250                        }
2251                    }
2252                    // Validate separator: must be a string if provided.
2253                    let separator = match effective_args.get(1) {
2254                        None | Some(JValue::Undefined) => None,
2255                        Some(JValue::String(s)) => Some(&**s),
2256                        Some(_) => {
2257                            return Err(EvaluatorError::TypeError(
2258                                "T0410: Argument 2 of function $join does not match function signature (expected String)"
2259                                    .to_string(),
2260                            ))
2261                        }
2262                    };
2263                    Ok(functions::string::join(arr, separator)?)
2264                }
2265                JValue::String(s) => Ok(JValue::String(s.clone())),
2266                _ => Err(EvaluatorError::TypeError(
2267                    "T0412: Argument 1 of function $join must be an array of String".to_string(),
2268                )),
2269            }
2270        }
2271
2272        // ── Numeric functions ───────────────────────────────────────────
2273        "number" => match effective_args.first() {
2274            Some(v) => Ok(functions::numeric::number(v)?),
2275            None => Err(EvaluatorError::EvaluationError(
2276                "number() requires at least 1 argument".to_string(),
2277            )),
2278        },
2279        "floor" => match effective_args.first() {
2280            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2281            Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
2282            _ => Err(EvaluatorError::TypeError(
2283                "floor() requires a number argument".to_string(),
2284            )),
2285        },
2286        "ceil" => match effective_args.first() {
2287            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2288            Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
2289            _ => Err(EvaluatorError::TypeError(
2290                "ceil() requires a number argument".to_string(),
2291            )),
2292        },
2293        "round" => match effective_args.first() {
2294            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2295            Some(JValue::Number(n)) => {
2296                let precision = effective_args.get(1).and_then(|v| {
2297                    if let JValue::Number(p) = v {
2298                        Some(*p as i32)
2299                    } else {
2300                        None
2301                    }
2302                });
2303                Ok(functions::numeric::round(*n, precision)?)
2304            }
2305            _ => Err(EvaluatorError::TypeError(
2306                "round() requires a number argument".to_string(),
2307            )),
2308        },
2309        "abs" => match effective_args.first() {
2310            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2311            Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
2312            _ => Err(EvaluatorError::TypeError(
2313                "abs() requires a number argument".to_string(),
2314            )),
2315        },
2316        "sqrt" => match effective_args.first() {
2317            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2318            Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
2319            _ => Err(EvaluatorError::TypeError(
2320                "sqrt() requires a number argument".to_string(),
2321            )),
2322        },
2323
2324        // ── Aggregation functions ───────────────────────────────────────
2325        "sum" => match effective_args.first() {
2326            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2327            None => Err(EvaluatorError::EvaluationError(
2328                "sum() requires exactly 1 argument".to_string(),
2329            )),
2330            Some(JValue::Null) => Ok(JValue::Null),
2331            Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
2332            Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
2333            Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
2334        },
2335        "max" => match effective_args.first() {
2336            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2337            Some(JValue::Null) | None => Ok(JValue::Null),
2338            Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
2339            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2340            _ => Err(EvaluatorError::TypeError(
2341                "max() requires an array or number argument".to_string(),
2342            )),
2343        },
2344        "min" => match effective_args.first() {
2345            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2346            Some(JValue::Null) | None => Ok(JValue::Null),
2347            Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
2348            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2349            _ => Err(EvaluatorError::TypeError(
2350                "min() requires an array or number argument".to_string(),
2351            )),
2352        },
2353        "average" => match effective_args.first() {
2354            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2355            Some(JValue::Null) | None => Ok(JValue::Null),
2356            Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2357            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2358            _ => Err(EvaluatorError::TypeError(
2359                "average() requires an array or number argument".to_string(),
2360            )),
2361        },
2362        "count" => match effective_args.first() {
2363            Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2364            Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2365            Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2366            _ => Ok(JValue::from(1i64)),
2367        },
2368
2369        // ── Boolean / logic ─────────────────────────────────────────────
2370        "boolean" => match effective_args.first() {
2371            Some(v) => Ok(functions::boolean::boolean(v)?),
2372            None => Err(EvaluatorError::EvaluationError(
2373                "boolean() requires 1 argument".to_string(),
2374            )),
2375        },
2376        "not" => match effective_args.first() {
2377            Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2378            None => Err(EvaluatorError::EvaluationError(
2379                "not() requires 1 argument".to_string(),
2380            )),
2381        },
2382
2383        // ── Array functions ─────────────────────────────────────────────
2384        "append" => {
2385            if effective_args.len() != 2 {
2386                return Err(EvaluatorError::EvaluationError(
2387                    "append() requires exactly 2 arguments".to_string(),
2388                ));
2389            }
2390            let first = &effective_args[0];
2391            let second = &effective_args[1];
2392            if matches!(second, JValue::Null | JValue::Undefined) {
2393                return Ok(first.clone());
2394            }
2395            if matches!(first, JValue::Null | JValue::Undefined) {
2396                return Ok(second.clone());
2397            }
2398            let arr = match first {
2399                JValue::Array(a) => a.to_vec(),
2400                other => vec![other.clone()],
2401            };
2402            let second_len = match second {
2403                JValue::Array(a) => a.len(),
2404                _ => 1,
2405            };
2406            check_sequence_length(arr.len() + second_len, options)?;
2407            Ok(functions::array::append(&arr, second)?)
2408        }
2409        "reverse" => match effective_args.first() {
2410            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2411            Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2412            _ => Err(EvaluatorError::TypeError(
2413                "reverse() requires an array argument".to_string(),
2414            )),
2415        },
2416        "distinct" => match effective_args.first() {
2417            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2418            Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2419            // Non-array input, and arrays of length <= 1, pass through unchanged
2420            // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2421            Some(other) => Ok(other.clone()),
2422        },
2423
2424        // ── Object functions ────────────────────────────────────────────
2425        "keys" => match effective_args.first() {
2426            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2427            Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2428            Some(JValue::Object(obj)) => {
2429                if obj.is_empty() {
2430                    Ok(JValue::Null)
2431                } else {
2432                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2433                    check_sequence_length(keys.len(), options)?;
2434                    if keys.len() == 1 {
2435                        Ok(keys.into_iter().next().unwrap())
2436                    } else {
2437                        Ok(JValue::array(keys))
2438                    }
2439                }
2440            }
2441            Some(JValue::Array(arr)) => {
2442                let mut all_keys: Vec<JValue> = Vec::new();
2443                for item in arr.iter() {
2444                    let normalized_item = normalize_lazy(item)?;
2445                    if let JValue::Object(obj) = &normalized_item {
2446                        for key in obj.keys() {
2447                            let k = JValue::string(key.clone());
2448                            if !all_keys.contains(&k) {
2449                                all_keys.push(k);
2450                            }
2451                        }
2452                    }
2453                }
2454                if all_keys.is_empty() {
2455                    Ok(JValue::Null)
2456                } else if all_keys.len() == 1 {
2457                    Ok(all_keys.into_iter().next().unwrap())
2458                } else {
2459                    check_sequence_length(all_keys.len(), options)?;
2460                    Ok(JValue::array(all_keys))
2461                }
2462            }
2463            _ => Ok(JValue::Null),
2464        },
2465        "merge" => match effective_args.len() {
2466            0 => Err(EvaluatorError::EvaluationError(
2467                "merge() requires at least 1 argument".to_string(),
2468            )),
2469            1 => match &effective_args[0] {
2470                JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2471                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2472                JValue::Object(_) => Ok(effective_args[0].clone()),
2473                _ => Err(EvaluatorError::TypeError(
2474                    "merge() requires objects or an array of objects".to_string(),
2475                )),
2476            },
2477            _ => Ok(functions::object::merge(effective_args)?),
2478        },
2479
2480        _ => unreachable!(
2481            "call_pure_builtin called with non-compilable builtin: {}",
2482            name
2483        ),
2484    }
2485}
2486
2487// ──────────────────────────────────────────────────────────────────────────────
2488// End of CompiledExpr framework
2489// ──────────────────────────────────────────────────────────────────────────────
2490
2491/// Functions that propagate undefined (return undefined when given an undefined argument).
2492/// These functions should return null/undefined when their input path doesn't exist,
2493/// rather than throwing a type error.
2494const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2495    "not",
2496    "boolean",
2497    "length",
2498    "number",
2499    "uppercase",
2500    "lowercase",
2501    "substring",
2502    "substringBefore",
2503    "substringAfter",
2504    "string",
2505    "abs",
2506    "ceil",
2507    "floor",
2508    "round",
2509    "sqrt",
2510];
2511
2512/// Check whether a function propagates undefined values
2513fn propagates_undefined(name: &str) -> bool {
2514    UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2515}
2516
2517/// Iterator-based numeric aggregation helpers.
2518/// These avoid cloning values by iterating over references and extracting f64 values directly.
2519mod aggregation {
2520    use super::*;
2521
2522    /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2523    /// Returns Err if any non-numeric value is encountered.
2524    fn for_each_numeric(
2525        arr: &[JValue],
2526        func_name: &str,
2527        mut f: impl FnMut(f64),
2528    ) -> Result<(), EvaluatorError> {
2529        fn recurse(
2530            arr: &[JValue],
2531            func_name: &str,
2532            f: &mut dyn FnMut(f64),
2533        ) -> Result<(), EvaluatorError> {
2534            for value in arr.iter() {
2535                match value {
2536                    JValue::Array(inner) => recurse(inner, func_name, f)?,
2537                    JValue::Number(n) => {
2538                        f(*n);
2539                    }
2540                    _ => {
2541                        return Err(EvaluatorError::TypeError(format!(
2542                            "{}() requires all array elements to be numbers",
2543                            func_name
2544                        )));
2545                    }
2546                }
2547            }
2548            Ok(())
2549        }
2550        recurse(arr, func_name, &mut f)
2551    }
2552
2553    /// Count elements in a potentially nested array without cloning.
2554    fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2555        let mut count = 0usize;
2556        for_each_numeric(arr, func_name, |_| count += 1)?;
2557        Ok(count)
2558    }
2559
2560    pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2561        if arr.is_empty() {
2562            return Ok(JValue::from(0i64));
2563        }
2564        let mut total = 0.0f64;
2565        for_each_numeric(arr, "sum", |n| total += n)?;
2566        Ok(JValue::Number(total))
2567    }
2568
2569    pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2570        if arr.is_empty() {
2571            return Ok(JValue::Null);
2572        }
2573        let mut max_val = f64::NEG_INFINITY;
2574        for_each_numeric(arr, "max", |n| {
2575            if n > max_val {
2576                max_val = n;
2577            }
2578        })?;
2579        Ok(JValue::Number(max_val))
2580    }
2581
2582    pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2583        if arr.is_empty() {
2584            return Ok(JValue::Null);
2585        }
2586        let mut min_val = f64::INFINITY;
2587        for_each_numeric(arr, "min", |n| {
2588            if n < min_val {
2589                min_val = n;
2590            }
2591        })?;
2592        Ok(JValue::Number(min_val))
2593    }
2594
2595    pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2596        if arr.is_empty() {
2597            return Ok(JValue::Null);
2598        }
2599        let mut total = 0.0f64;
2600        let count = count_numeric(arr, "average")?;
2601        for_each_numeric(arr, "average", |n| total += n)?;
2602        Ok(JValue::Number(total / count as f64))
2603    }
2604}
2605
2606/// Evaluator errors
2607#[derive(Error, Debug)]
2608pub enum EvaluatorError {
2609    #[error("Type error: {0}")]
2610    TypeError(String),
2611
2612    #[error("Reference error: {0}")]
2613    ReferenceError(String),
2614
2615    #[error("Evaluation error: {0}")]
2616    EvaluationError(String),
2617
2618    /// Python→JValue conversion failed during lazy field access.
2619    /// Surfaces as Python TypeError at the boundary (matching what eager
2620    /// conversion would have raised at call time).
2621    #[cfg(feature = "python")]
2622    #[error("Type error: {0}")]
2623    PyConversionError(String),
2624}
2625
2626impl From<crate::functions::FunctionError> for EvaluatorError {
2627    fn from(e: crate::functions::FunctionError) -> Self {
2628        // `PyConversionError` must surface as a Python `TypeError` (matching what
2629        // eager conversion would have raised), not the generic `ValueError` every
2630        // other `FunctionError` variant maps to below -- see its doc comment.
2631        #[cfg(feature = "python")]
2632        if let crate::functions::FunctionError::PyConversionError(m) = &e {
2633            return EvaluatorError::PyConversionError(m.clone());
2634        }
2635        EvaluatorError::EvaluationError(e.to_string())
2636    }
2637}
2638
2639impl From<crate::datetime::DateTimeError> for EvaluatorError {
2640    fn from(e: crate::datetime::DateTimeError) -> Self {
2641        EvaluatorError::EvaluationError(e.to_string())
2642    }
2643}
2644
2645impl EvaluatorError {
2646    /// The underlying message, without the outer "Type error: "/
2647    /// "Reference error: "/"Evaluation error: " prefix that `Display` (via
2648    /// thiserror's `#[error("Type error: {0}")]` etc.) would add. This is
2649    /// what JSONata-spec-coded messages like "T2002: ..." actually look
2650    /// like — the coded prefix is INSIDE this string, not added by
2651    /// `Display`. Used by both the Python bindings (`src/lib.rs`) and the
2652    /// `jsonata` CLI so the two never need to duplicate this unwrap.
2653    pub fn message(&self) -> &str {
2654        match self {
2655            EvaluatorError::TypeError(m) => m,
2656            EvaluatorError::ReferenceError(m) => m,
2657            EvaluatorError::EvaluationError(m) => m,
2658            #[cfg(feature = "python")]
2659            EvaluatorError::PyConversionError(m) => m,
2660        }
2661    }
2662}
2663
2664#[cfg(feature = "python")]
2665impl From<crate::lazy::LazyConvertError> for EvaluatorError {
2666    fn from(e: crate::lazy::LazyConvertError) -> Self {
2667        EvaluatorError::PyConversionError(e.0)
2668    }
2669}
2670
2671#[cfg(test)]
2672mod evaluator_error_message_tests {
2673    use super::EvaluatorError;
2674
2675    #[test]
2676    fn message_strips_the_display_prefix() {
2677        let e = EvaluatorError::TypeError(
2678            "T2002: The left side of the + operator must evaluate to a number".to_string(),
2679        );
2680        assert_eq!(
2681            e.message(),
2682            "T2002: The left side of the + operator must evaluate to a number"
2683        );
2684        // Display, by contrast, adds the "Type error: " wrapper -- this is
2685        // exactly the distinction `message()` exists to avoid.
2686        assert_eq!(
2687            e.to_string(),
2688            "Type error: T2002: The left side of the + operator must evaluate to a number"
2689        );
2690    }
2691
2692    #[test]
2693    fn message_works_for_all_variants() {
2694        assert_eq!(
2695            EvaluatorError::ReferenceError("$foo is not defined".to_string()).message(),
2696            "$foo is not defined"
2697        );
2698        assert_eq!(
2699            EvaluatorError::EvaluationError("something went wrong".to_string()).message(),
2700            "something went wrong"
2701        );
2702    }
2703}
2704
2705/// Result of evaluating a lambda body that may be a tail call
2706/// Used for trampoline-based tail call optimization
2707enum LambdaResult {
2708    /// Final value - evaluation is complete
2709    JValue(JValue),
2710    /// Tail call - need to continue with another lambda invocation
2711    TailCall {
2712        /// The lambda to call (boxed to reduce enum size)
2713        lambda: Box<StoredLambda>,
2714        /// Arguments for the call
2715        args: Vec<JValue>,
2716        /// Data context for the call
2717        data: JValue,
2718    },
2719}
2720
2721/// Lambda storage
2722/// Stores the AST of a lambda function along with its parameters, optional signature,
2723/// and captured environment for closures
2724#[derive(Clone, Debug)]
2725pub struct StoredLambda {
2726    pub params: Vec<String>,
2727    pub body: AstNode,
2728    /// Pre-compiled body for use in tight inner loops (HOF fast path).
2729    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2730    pub(crate) compiled_body: Option<CompiledExpr>,
2731    pub signature: Option<String>,
2732    /// Captured environment bindings for closures
2733    pub captured_env: HashMap<String, JValue>,
2734    /// Captured data context for lexical scoping of bare field names
2735    pub captured_data: Option<JValue>,
2736    /// Whether this lambda's body contains tail calls that can be optimized
2737    pub thunk: bool,
2738}
2739
2740/// A single scope in the scope stack
2741struct Scope {
2742    bindings: HashMap<String, JValue>,
2743    lambdas: HashMap<String, StoredLambda>,
2744}
2745
2746impl Scope {
2747    fn new() -> Self {
2748        Scope {
2749            bindings: HashMap::new(),
2750            lambdas: HashMap::new(),
2751        }
2752    }
2753}
2754
2755/// Evaluation context
2756///
2757/// Holds variable bindings and other state needed during evaluation.
2758/// Uses a scope stack for efficient push/pop instead of clone/restore.
2759pub struct Context {
2760    scope_stack: Vec<Scope>,
2761    parent_data: Option<JValue>,
2762}
2763
2764impl Context {
2765    pub fn new() -> Self {
2766        Context {
2767            scope_stack: vec![Scope::new()],
2768            parent_data: None,
2769        }
2770    }
2771
2772    /// Push a new scope onto the stack
2773    fn push_scope(&mut self) {
2774        self.scope_stack.push(Scope::new());
2775    }
2776
2777    /// Pop the top scope from the stack
2778    fn pop_scope(&mut self) {
2779        if self.scope_stack.len() > 1 {
2780            self.scope_stack.pop();
2781        }
2782    }
2783
2784    /// Pop scope but preserve specified lambdas by moving them to the current top scope
2785    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2786        if self.scope_stack.len() > 1 {
2787            let popped = self.scope_stack.pop().unwrap();
2788            if !lambda_ids.is_empty() {
2789                let top = self.scope_stack.last_mut().unwrap();
2790                for id in lambda_ids {
2791                    if let Some(stored) = popped.lambdas.get(id) {
2792                        top.lambdas.insert(id.clone(), stored.clone());
2793                    }
2794                }
2795            }
2796        }
2797    }
2798
2799    /// Clear all bindings and lambdas in the top scope without deallocating
2800    fn clear_current_scope(&mut self) {
2801        let top = self.scope_stack.last_mut().unwrap();
2802        top.bindings.clear();
2803        top.lambdas.clear();
2804    }
2805
2806    pub fn bind(&mut self, name: String, value: JValue) {
2807        self.scope_stack
2808            .last_mut()
2809            .unwrap()
2810            .bindings
2811            .insert(name, value);
2812    }
2813
2814    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2815        self.scope_stack
2816            .last_mut()
2817            .unwrap()
2818            .lambdas
2819            .insert(name, lambda);
2820    }
2821
2822    pub fn unbind(&mut self, name: &str) {
2823        // Remove from top scope only
2824        let top = self.scope_stack.last_mut().unwrap();
2825        top.bindings.remove(name);
2826        top.lambdas.remove(name);
2827    }
2828
2829    pub fn lookup(&self, name: &str) -> Option<&JValue> {
2830        // Walk scope stack from top to bottom
2831        for scope in self.scope_stack.iter().rev() {
2832            if let Some(value) = scope.bindings.get(name) {
2833                return Some(value);
2834            }
2835        }
2836        None
2837    }
2838
2839    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2840        // Walk scope stack from top to bottom
2841        for scope in self.scope_stack.iter().rev() {
2842            if let Some(lambda) = scope.lambdas.get(name) {
2843                return Some(lambda);
2844            }
2845        }
2846        None
2847    }
2848
2849    pub fn set_parent(&mut self, data: JValue) {
2850        self.parent_data = Some(data);
2851    }
2852
2853    pub fn get_parent(&self) -> Option<&JValue> {
2854        self.parent_data.as_ref()
2855    }
2856
2857    /// Collect all bindings across all scopes (for environment capture).
2858    /// Higher scopes shadow lower scopes.
2859    fn all_bindings(&self) -> HashMap<String, JValue> {
2860        let mut result = HashMap::new();
2861        for scope in &self.scope_stack {
2862            for (k, v) in &scope.bindings {
2863                result.insert(k.clone(), v.clone());
2864            }
2865        }
2866        result
2867    }
2868}
2869
2870impl Default for Context {
2871    fn default() -> Self {
2872        Self::new()
2873    }
2874}
2875
2876/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2877/// ...}`) from a value about to leave the evaluator.
2878///
2879/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2880/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2881/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2882/// path step consumes and re-wraps these as evaluation proceeds, but the
2883/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2884/// because the tuple-producing expression itself is the whole result (a bare
2885/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2886/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2887/// wrapper ends up embedded in a field value or array element rather than at
2888/// the top level. This recurses through both array elements and (non-tuple)
2889/// object field values so both shapes are cleaned up, not just a bare
2890/// top-level tuple array.
2891/// Merge a group of tuple wrappers into a single tuple, appending each key's
2892/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2893/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2894/// a key present in one tuple stays a scalar; a key present in several becomes an
2895/// array of the collected values (used by group-by value evaluation so a group
2896/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2897/// N collected focus values).
2898fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2899    fn append(acc: Option<JValue>, v: JValue) -> JValue {
2900        match acc {
2901            None => v,
2902            Some(a) => {
2903                let mut out: Vec<JValue> = match a {
2904                    JValue::Array(arr) => arr.iter().cloned().collect(),
2905                    other => vec![other],
2906                };
2907                match v {
2908                    JValue::Array(arr) => out.extend(arr.iter().cloned()),
2909                    other => out.push(other),
2910                }
2911                JValue::array(out)
2912            }
2913        }
2914    }
2915    let mut result: IndexMap<String, JValue> = IndexMap::new();
2916    for tuple in group {
2917        if let JValue::Object(obj) = tuple {
2918            for (k, v) in obj.iter() {
2919                if k == "__tuple__" {
2920                    result.insert(k.clone(), v.clone());
2921                    continue;
2922                }
2923                let merged = append(result.shift_remove(k), v.clone());
2924                result.insert(k.clone(), merged);
2925            }
2926        }
2927    }
2928    result
2929}
2930
2931fn unwrap_tuple_output(value: JValue) -> JValue {
2932    match value {
2933        JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2934            .get("@")
2935            .cloned()
2936            .map(unwrap_tuple_output)
2937            .unwrap_or(JValue::Undefined),
2938        JValue::Object(obj) => {
2939            let mut new_map = IndexMap::with_capacity(obj.len());
2940            for (k, v) in obj.iter() {
2941                new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2942            }
2943            JValue::object(new_map)
2944        }
2945        JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2946        other => other,
2947    }
2948}
2949
2950/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2951/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2952/// (if anything) was bound under that name beforehand. `restore` puts the
2953/// prior value back -- or removes the binding entirely if there wasn't
2954/// one -- rather than unconditionally unbinding, so a tuple key that
2955/// happens to share a name with a live outer `:=` binding in the same
2956/// scope frame doesn't get permanently deleted once the tuple-row
2957/// evaluation finishes.
2958struct TupleKeyBindings {
2959    saved: Vec<(String, Option<JValue>)>,
2960}
2961
2962impl TupleKeyBindings {
2963    /// True if `name` was one of the keys this guard bound (used by callers
2964    /// that need to know whether a given tuple key is already in scope
2965    /// before binding it a second time under a different role, e.g.
2966    /// `create_tuple_stream`'s ancestor-label handling).
2967    fn contains(&self, name: &str) -> bool {
2968        self.saved.iter().any(|(n, _)| n == name)
2969    }
2970
2971    fn restore(self, evaluator: &mut Evaluator) {
2972        for (name, prior) in self.saved {
2973            match prior {
2974                Some(value) => evaluator.context.bind(name, value),
2975                None => evaluator.context.unbind(&name),
2976            }
2977        }
2978    }
2979}
2980
2981/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
2982/// evaluator options. All fields default to `None` = unlimited (current behavior).
2983#[derive(Default, Clone, Debug)]
2984pub struct EvaluatorOptions {
2985    /// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
2986    pub timeout_ms: Option<u64>,
2987    /// Maximum AST-recursion stack depth. Exceeding it raises D1011 if this is the
2988    /// tighter of this value and the hardcoded native-stack safety ceiling (302);
2989    /// otherwise the hardcoded ceiling still raises U1001 (see GitHub issue #34).
2990    pub max_stack_depth: Option<usize>,
2991    /// Maximum length of a query-result sequence (map/filter/wildcard/descendants/
2992    /// keys/lookup/append/spread/each/range/path-mapping). Exceeding it raises D2015.
2993    /// Does NOT currently apply to literal array construction (`MakeArray`/
2994    /// `ArrayConstruct`) — NOTE this is a deliberate, temporary divergence from
2995    /// upstream, not a match: jsonata-js DOES cap flat/non-nested array literals
2996    /// (via `fn.append`'s `createSequence` hook in `evaluateUnary`'s `[` case).
2997    /// Deferred until the separate `MakeArray(u16)` truncation bug is fixed (see
2998    /// the design spec's "Sequence length → D2015" section).
2999    pub max_sequence_length: Option<usize>,
3000}
3001
3002/// Checks a constructed query-result sequence's length against the configured
3003/// `max_sequence_length` guardrail. Call this at sites that build a query-result
3004/// sequence (map/filter/wildcard/descendants/keys/lookup/append/spread/each/range/
3005/// path-mapping). NOT currently called at literal array construction (`[1,2,3]`) —
3006/// unlike upstream jsonata-js, which caps flat/non-nested literals too via
3007/// `fn.append`'s `createSequence()` hook. See `EvaluatorOptions::max_sequence_length`
3008/// doc comment above for why this is a deliberate, temporary gap.
3009pub(crate) fn check_sequence_length(
3010    len: usize,
3011    options: &EvaluatorOptions,
3012) -> Result<(), EvaluatorError> {
3013    if let Some(max) = options.max_sequence_length {
3014        if len > max {
3015            return Err(EvaluatorError::EvaluationError(format!(
3016                "D2015: The maximum sequence length of {} was exceeded.",
3017                max
3018            )));
3019        }
3020    }
3021    Ok(())
3022}
3023
3024/// Per-iteration D1012 check for loop-based compiled/VM constructs (map/filter/
3025/// reduce element loops, FilterByBytecode) that don't pass through
3026/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed.
3027#[inline]
3028pub(crate) fn check_loop_timeout(
3029    options: &EvaluatorOptions,
3030    start_time: Option<Instant>,
3031) -> Result<(), EvaluatorError> {
3032    if let Some(timeout_ms) = options.timeout_ms {
3033        if let Some(start) = start_time {
3034            if start.elapsed().as_millis() as u64 > timeout_ms {
3035                return Err(EvaluatorError::EvaluationError(format!(
3036                    "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3037                    timeout_ms
3038                )));
3039            }
3040        }
3041    }
3042    Ok(())
3043}
3044
3045/// Evaluator for JSONata expressions
3046pub struct Evaluator {
3047    context: Context,
3048    recursion_depth: usize,
3049    max_recursion_depth: usize,
3050    /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
3051    /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
3052    /// using the AST node's pointer address (as before) collided whenever the same
3053    /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
3054    /// or other repeated recursion), aliasing unrelated closures that shared an id.
3055    next_lambda_id: u64,
3056    /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
3057    /// wrapper during this top-level `evaluate()` call. Reset at the start of
3058    /// `evaluate()` and checked at the end to decide whether the (recursive,
3059    /// O(result size)) tuple-unwrap pass is needed before returning to the
3060    /// caller — keeps the vast majority of evaluations, which never touch
3061    /// `%`/`@`/`#`, at zero added cost.
3062    tuple_stream_created: bool,
3063    /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
3064    /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
3065    /// by the two consumers that read those carried bindings directly from the
3066    /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
3067    /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
3068    /// evaluating its input path (key/value expressions read `$focus` off the
3069    /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
3070    /// of projecting each tuple's `@`.
3071    keep_tuple_stream: bool,
3072    options: EvaluatorOptions,
3073    /// Set in `evaluate()` (only when `options.timeout_ms` is configured) and
3074    /// checked in `evaluate_internal`'s per-node checkpoint for D1012.
3075    start_time: Option<Instant>,
3076}
3077
3078impl Evaluator {
3079    pub fn new() -> Self {
3080        Evaluator {
3081            context: Context::new(),
3082            recursion_depth: 0,
3083            // Limit recursion depth to prevent stack overflow
3084            // True TCO would allow deeper recursion but requires parser-level thunk marking
3085            max_recursion_depth: 302,
3086            next_lambda_id: 0,
3087            tuple_stream_created: false,
3088            keep_tuple_stream: false,
3089            options: EvaluatorOptions::default(),
3090            start_time: None,
3091        }
3092    }
3093
3094    pub fn with_context(context: Context) -> Self {
3095        Evaluator {
3096            context,
3097            recursion_depth: 0,
3098            max_recursion_depth: 302,
3099            next_lambda_id: 0,
3100            tuple_stream_created: false,
3101            keep_tuple_stream: false,
3102            options: EvaluatorOptions::default(),
3103            start_time: None,
3104        }
3105    }
3106
3107    /// Construct an `Evaluator` with guardrail options. `Evaluator::new()`/
3108    /// `with_context()` remain unchanged (unlimited options) for existing callers.
3109    pub fn with_options(context: Context, options: EvaluatorOptions) -> Self {
3110        Evaluator {
3111            context,
3112            recursion_depth: 0,
3113            max_recursion_depth: 302,
3114            next_lambda_id: 0,
3115            tuple_stream_created: false,
3116            keep_tuple_stream: false,
3117            options,
3118            start_time: None,
3119        }
3120    }
3121
3122    /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
3123    fn fresh_lambda_id(&mut self) -> u64 {
3124        let id = self.next_lambda_id;
3125        self.next_lambda_id += 1;
3126        id
3127    }
3128
3129    /// Invoke a stored lambda with its captured environment and data.
3130    /// This is the standard way to call a StoredLambda, handling the
3131    /// captured_env and captured_data extraction boilerplate.
3132    fn invoke_stored_lambda(
3133        &mut self,
3134        stored: &StoredLambda,
3135        args: &[JValue],
3136        data: &JValue,
3137    ) -> Result<JValue, EvaluatorError> {
3138        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
3139        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
3140        // and no captured lambda/builtin values (those require Context for runtime lookup).
3141        if let Some(ref ce) = stored.compiled_body {
3142            if stored.signature.is_none()
3143                && !stored.thunk
3144                && !stored
3145                    .captured_env
3146                    .values()
3147                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
3148            {
3149                let call_data = stored.captured_data.as_ref().unwrap_or(data);
3150                let vars: HashMap<&str, &JValue> = stored
3151                    .params
3152                    .iter()
3153                    .zip(args.iter())
3154                    .map(|(p, v)| (p.as_str(), v))
3155                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
3156                    .collect();
3157                return eval_compiled(ce, call_data, Some(&vars), &self.options, self.start_time);
3158            }
3159        }
3160
3161        let captured_env = if stored.captured_env.is_empty() {
3162            None
3163        } else {
3164            Some(&stored.captured_env)
3165        };
3166        let captured_data = stored.captured_data.as_ref();
3167        self.invoke_lambda_with_env(
3168            &stored.params,
3169            &stored.body,
3170            stored.signature.as_ref(),
3171            args,
3172            data,
3173            captured_env,
3174            captured_data,
3175            stored.thunk,
3176        )
3177    }
3178
3179    /// Look up a StoredLambda from a JValue that may be a lambda marker.
3180    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
3181    /// with a valid lambda_id that references a stored lambda.
3182    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
3183        if let JValue::Lambda { lambda_id, .. } = value {
3184            return self.context.lookup_lambda(lambda_id).cloned();
3185        }
3186        None
3187    }
3188
3189    /// Get the number of parameters a callback function expects by inspecting its AST.
3190    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
3191    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
3192    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
3193        match func_node {
3194            AstNode::Lambda { params, .. } => params.len(),
3195            AstNode::Variable(var_name) => {
3196                // Check if this variable holds a stored lambda
3197                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
3198                    return stored_lambda.params.len();
3199                }
3200                // Also check if it's a lambda value in bindings (e.g., from partial application)
3201                if let Some(value) = self.context.lookup(var_name) {
3202                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
3203                        return stored_lambda.params.len();
3204                    }
3205                }
3206                // Unknown, return max to be safe
3207                usize::MAX
3208            }
3209            AstNode::Function { .. } => {
3210                // For function references, we can't easily determine param count
3211                // Return max to be safe
3212                usize::MAX
3213            }
3214            _ => usize::MAX,
3215        }
3216    }
3217
3218    /// Specialized sort using pre-extracted keys (Schwartzian transform).
3219    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
3220    /// avoiding O(N log N) hash lookups during comparisons.
3221    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
3222        if arr.len() <= 1 {
3223            return;
3224        }
3225
3226        // Phase 1: Extract sort keys -- one IndexMap lookup per element
3227        let keys: Vec<SortKey> = arr
3228            .iter()
3229            .map(|item| match item {
3230                JValue::Object(obj) => match obj.get(&spec.field) {
3231                    Some(JValue::Number(n)) => SortKey::Num(*n),
3232                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
3233                    _ => SortKey::None,
3234                },
3235                #[cfg(feature = "python")]
3236                JValue::LazyPyDict(lazy) => match lazy.get_field(&spec.field) {
3237                    Ok(JValue::Number(n)) => SortKey::Num(n),
3238                    Ok(JValue::String(s)) => SortKey::Str(s.clone()),
3239                    // Err(_) (conversion failure) and any other value fall through to
3240                    // SortKey::None (treated as "missing", sorts last). This arm is only
3241                    // reachable for elements that survived upstream evaluation of the sort
3242                    // array (T2008 gate / a prior get_field on the same data), so a
3243                    // conversion error swallowed here cannot silently mis-sort *today* --
3244                    // if this specialized path is ever reached before such validation,
3245                    // this arm must be revisited to propagate the error instead.
3246                    _ => SortKey::None,
3247                },
3248                _ => SortKey::None,
3249            })
3250            .collect();
3251
3252        // Phase 2: Build index permutation sorted by pre-extracted keys
3253        let mut perm: Vec<usize> = (0..arr.len()).collect();
3254        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
3255
3256        // Phase 3: Apply permutation in-place via cycle-following
3257        let mut placed = vec![false; arr.len()];
3258        for i in 0..arr.len() {
3259            if placed[i] || perm[i] == i {
3260                continue;
3261            }
3262            let mut j = i;
3263            loop {
3264                let target = perm[j];
3265                placed[j] = true;
3266                if target == i {
3267                    break;
3268                }
3269                arr.swap(j, target);
3270                j = target;
3271            }
3272        }
3273    }
3274
3275    /// Merge sort implementation using a comparator function.
3276    /// This replaces the O(n²) bubble sort for better performance on large arrays.
3277    /// The comparator returns true if the first element should come AFTER the second.
3278    fn merge_sort_with_comparator(
3279        &mut self,
3280        arr: &mut [JValue],
3281        comparator: &AstNode,
3282        data: &JValue,
3283    ) -> Result<(), EvaluatorError> {
3284        if arr.len() <= 1 {
3285            return Ok(());
3286        }
3287
3288        // Try specialized fast path for simple field comparisons like
3289        // function($l, $r) { $l.price > $r.price }
3290        if let AstNode::Lambda { params, body, .. } = comparator {
3291            if params.len() >= 2 {
3292                if let Some(spec) = try_specialize_sort_comparator(body, &params[0], &params[1]) {
3293                    Self::merge_sort_specialized(arr, &spec);
3294                    return Ok(());
3295                }
3296            }
3297        }
3298
3299        let mid = arr.len() / 2;
3300
3301        // Sort left half
3302        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
3303
3304        // Sort right half
3305        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
3306
3307        // Merge the sorted halves
3308        let mut temp = Vec::with_capacity(arr.len());
3309        let (left, right) = arr.split_at(mid);
3310
3311        let mut i = 0;
3312        let mut j = 0;
3313
3314        // For lambda comparators, use a reusable scope to avoid
3315        // push_scope/pop_scope per comparison (~n log n total comparisons)
3316        if let AstNode::Lambda { params, body, .. } = comparator {
3317            if params.len() >= 2 {
3318                // Pre-clone param names once outside the loop
3319                let param0 = params[0].clone();
3320                let param1 = params[1].clone();
3321                self.context.push_scope();
3322                while i < left.len() && j < right.len() {
3323                    // Reuse scope: clear and rebind instead of push/pop
3324                    self.context.clear_current_scope();
3325                    self.context.bind(param0.clone(), left[i].clone());
3326                    self.context.bind(param1.clone(), right[j].clone());
3327
3328                    let cmp_result = self.evaluate_internal(body, data)?;
3329
3330                    if self.is_truthy(&cmp_result) {
3331                        temp.push(right[j].clone());
3332                        j += 1;
3333                    } else {
3334                        temp.push(left[i].clone());
3335                        i += 1;
3336                    }
3337                }
3338                self.context.pop_scope();
3339            } else {
3340                // Unexpected param count - fall back to generic path
3341                while i < left.len() && j < right.len() {
3342                    let cmp_result = self.apply_function(
3343                        comparator,
3344                        &[left[i].clone(), right[j].clone()],
3345                        data,
3346                    )?;
3347                    if self.is_truthy(&cmp_result) {
3348                        temp.push(right[j].clone());
3349                        j += 1;
3350                    } else {
3351                        temp.push(left[i].clone());
3352                        i += 1;
3353                    }
3354                }
3355            }
3356        } else {
3357            // Non-lambda comparator: use generic apply_function path
3358            while i < left.len() && j < right.len() {
3359                let cmp_result =
3360                    self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
3361                if self.is_truthy(&cmp_result) {
3362                    temp.push(right[j].clone());
3363                    j += 1;
3364                } else {
3365                    temp.push(left[i].clone());
3366                    i += 1;
3367                }
3368            }
3369        }
3370
3371        // Copy remaining elements
3372        temp.extend_from_slice(&left[i..]);
3373        temp.extend_from_slice(&right[j..]);
3374
3375        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
3376        for (i, val) in temp.into_iter().enumerate() {
3377            arr[i] = val;
3378        }
3379
3380        Ok(())
3381    }
3382
3383    /// Evaluate an AST node against data
3384    ///
3385    /// This is the main entry point for evaluation. It sets up the parent context
3386    /// to be the root data if not already set.
3387    ///
3388    /// Also the single choke point for stripping any lingering tuple-stream
3389    /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
3390    /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
3391    /// tuple-stream representation (see `create_tuple_stream`), and without this
3392    /// a bare (or object/array-nested) tuple-producing expression would leak
3393    /// that internal representation into user-visible output instead of the
3394    /// plain value.
3395    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
3396        // Set parent context to root data if not already set
3397        if self.context.get_parent().is_none() {
3398            self.context.set_parent(data.clone());
3399        }
3400
3401        if self.options.timeout_ms.is_some() {
3402            self.start_time = Some(Instant::now());
3403        }
3404
3405        self.tuple_stream_created = false;
3406        let result = self.evaluate_internal(node, data)?;
3407        Ok(if self.tuple_stream_created {
3408            unwrap_tuple_output(result)
3409        } else {
3410            result
3411        })
3412    }
3413
3414    /// Fast evaluation for leaf nodes that don't need recursion tracking.
3415    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
3416    /// Returns None for anything requiring the full evaluator.
3417    #[inline(always)]
3418    fn evaluate_leaf(
3419        &mut self,
3420        node: &AstNode,
3421        data: &JValue,
3422    ) -> Option<Result<JValue, EvaluatorError>> {
3423        match node {
3424            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
3425            AstNode::Number(n) => {
3426                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3427                    Some(Ok(JValue::from(*n as i64)))
3428                } else {
3429                    Some(Ok(JValue::Number(*n)))
3430                }
3431            }
3432            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
3433            AstNode::Null => Some(Ok(JValue::Null)),
3434            AstNode::Undefined => Some(Ok(JValue::Undefined)),
3435            AstNode::Name(field_name) => match data {
3436                // Array mapping and other cases need full evaluator
3437                JValue::Object(obj) => Some(Ok(obj
3438                    .get(field_name)
3439                    .cloned()
3440                    .unwrap_or(JValue::Undefined))),
3441                #[cfg(feature = "python")]
3442                JValue::LazyPyDict(lazy) => {
3443                    Some(lazy.get_field(field_name).map_err(EvaluatorError::from))
3444                }
3445                _ => None,
3446            },
3447            AstNode::Variable(name) if !name.is_empty() => {
3448                // Simple variable lookup — only fast-path when no tuple data
3449                if let JValue::Object(obj) = data {
3450                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3451                        return None; // Tuple data needs full evaluator
3452                    }
3453                }
3454                // May be a lambda/builtin — needs full evaluator if None
3455                self.context.lookup(name).map(|value| Ok(value.clone()))
3456            }
3457            _ => None,
3458        }
3459    }
3460
3461    /// Internal evaluation method
3462    fn evaluate_internal(
3463        &mut self,
3464        node: &AstNode,
3465        data: &JValue,
3466    ) -> Result<JValue, EvaluatorError> {
3467        // Fast path for leaf nodes — skip recursion tracking overhead
3468        if let Some(result) = self.evaluate_leaf(node, data) {
3469            return result;
3470        }
3471
3472        // Check recursion depth to prevent stack overflow. `effective_limit` is
3473        // whichever is tighter: the user's `max_stack_depth` guardrail or the
3474        // hardcoded native-stack-safety ceiling (`max_recursion_depth`, always
3475        // 302, GitHub issue #34). The hardcoded ceiling is an always-on backstop
3476        // regardless of user options — only a user limit BELOW it can produce
3477        // D1011; hitting the hardcoded ceiling itself (no option set, or an
3478        // option set at/above 302) still produces U1001.
3479        self.recursion_depth += 1;
3480        let effective_limit = match self.options.max_stack_depth {
3481            Some(limit) => limit.min(self.max_recursion_depth),
3482            None => self.max_recursion_depth,
3483        };
3484        if self.recursion_depth > effective_limit {
3485            self.recursion_depth -= 1;
3486            return Err(EvaluatorError::EvaluationError(
3487                if effective_limit < self.max_recursion_depth {
3488                    "D1011: Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive".to_string()
3489                } else {
3490                    format!(
3491                        "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
3492                        effective_limit
3493                    )
3494                },
3495            ));
3496        }
3497
3498        // Check evaluation timeout (D1012). `start_time` is only set (in
3499        // `evaluate()`) when `options.timeout_ms` is configured, so this is a
3500        // single `is_none()` branch of overhead when no timeout is set.
3501        if let Some(timeout_ms) = self.options.timeout_ms {
3502            if let Some(start) = self.start_time {
3503                if start.elapsed().as_millis() as u64 > timeout_ms {
3504                    self.recursion_depth -= 1;
3505                    return Err(EvaluatorError::EvaluationError(format!(
3506                        "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3507                        timeout_ms
3508                    )));
3509                }
3510            }
3511        }
3512
3513        // The soft depth counter above is calibrated against a comfortably
3514        // large native stack. Hosts with a much smaller default thread stack
3515        // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
3516        // stack well before this counter trips, crashing the process instead
3517        // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
3518        // transparently swaps in a bigger stack segment when headroom is
3519        // low, so this stays a no-op cost on the common shallow path.
3520        const RED_ZONE: usize = 128 * 1024;
3521        const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
3522        let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
3523            self.evaluate_internal_impl(node, data)
3524        });
3525
3526        self.recursion_depth -= 1;
3527        result
3528    }
3529
3530    /// Internal evaluation implementation (separated to allow depth tracking)
3531    fn evaluate_internal_impl(
3532        &mut self,
3533        node: &AstNode,
3534        data: &JValue,
3535    ) -> Result<JValue, EvaluatorError> {
3536        match node {
3537            AstNode::String(s) => Ok(JValue::string(s.clone())),
3538
3539            // Name nodes represent field access on the current data
3540            AstNode::Name(field_name) => {
3541                match data {
3542                    JValue::Object(obj) => {
3543                        Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3544                    }
3545                    JValue::Array(arr) => {
3546                        // Map over array
3547                        let mut result = Vec::new();
3548                        for item in arr.iter() {
3549                            if let JValue::Object(obj) = item {
3550                                if let Some(val) = obj.get(field_name) {
3551                                    result.push(val.clone());
3552                                }
3553                            }
3554                        }
3555                        if result.is_empty() {
3556                            Ok(JValue::Undefined)
3557                        } else if result.len() == 1 {
3558                            Ok(result.into_iter().next().unwrap())
3559                        } else {
3560                            Ok(JValue::array(result))
3561                        }
3562                    }
3563                    _ => Ok(JValue::Undefined),
3564                }
3565            }
3566
3567            AstNode::Number(n) => {
3568                // Preserve integer-ness: if the number is a whole number, create an integer JValue
3569                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3570                    // It's a whole number that can be represented as i64
3571                    Ok(JValue::from(*n as i64))
3572                } else {
3573                    Ok(JValue::Number(*n))
3574                }
3575            }
3576            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3577            AstNode::Null => Ok(JValue::Null),
3578            AstNode::Undefined => Ok(JValue::Undefined),
3579            AstNode::Placeholder => {
3580                // Placeholders should only appear as function arguments
3581                // If we reach here, it's an error
3582                Err(EvaluatorError::EvaluationError(
3583                    "Placeholder '?' can only be used as a function argument".to_string(),
3584                ))
3585            }
3586            AstNode::Regex { pattern, flags } => {
3587                // Return a regex object as a special JSON value
3588                // This will be recognized by functions like $split, $match, $replace
3589                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3590            }
3591
3592            AstNode::Variable(name) => {
3593                // Special case: $ alone (empty name) refers to current context
3594                // First check if $ is bound in the context (for closures that captured $)
3595                // Otherwise, use the data parameter
3596                if name.is_empty() {
3597                    if let Some(value) = self.context.lookup("$") {
3598                        return Ok(value.clone());
3599                    }
3600                    // If data is a tuple, return the @ value
3601                    if let JValue::Object(obj) = data {
3602                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3603                            if let Some(inner) = obj.get("@") {
3604                                return Ok(inner.clone());
3605                            }
3606                        }
3607                    }
3608                    return Ok(data.clone());
3609                }
3610
3611                // Check variable bindings FIRST
3612                // This allows function parameters to shadow outer lambdas with the same name
3613                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3614                if let Some(value) = self.context.lookup(name) {
3615                    return Ok(value.clone());
3616                }
3617
3618                // Check tuple bindings in data (for index binding operator #$var)
3619                // When iterating over a tuple stream, $var can reference the bound index
3620                if let JValue::Object(obj) = data {
3621                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3622                        // Check for the variable in tuple bindings (stored as "$name")
3623                        let binding_key = format!("${}", name);
3624                        if let Some(binding_value) = obj.get(&binding_key) {
3625                            return Ok(binding_value.clone());
3626                        }
3627                    }
3628                }
3629
3630                // Then check if this is a stored lambda (user-defined functions)
3631                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3632                    // Return a lambda representation that can be passed to higher-order functions
3633                    // Include _lambda_id pointing to the stored lambda so it can be found
3634                    // when captured in closures
3635                    let lambda_repr = JValue::lambda(
3636                        name.as_str(),
3637                        stored_lambda.params.clone(),
3638                        Some(name.to_string()),
3639                        stored_lambda.signature.clone(),
3640                    );
3641                    return Ok(lambda_repr);
3642                }
3643
3644                // Check if this is a built-in function reference (only if not shadowed)
3645                if self.is_builtin_function(name) {
3646                    // Return a marker for built-in functions
3647                    // This allows built-in functions to be passed to higher-order functions
3648                    let builtin_repr = JValue::builtin(name.as_str());
3649                    return Ok(builtin_repr);
3650                }
3651
3652                // Undefined variable - return null (undefined in JSONata semantics)
3653                // This allows expressions like `$not(undefined_var)` to return undefined
3654                // and comparisons like `3 > $undefined` to return undefined
3655                Ok(JValue::Null)
3656            }
3657
3658            AstNode::ParentVariable(name) => {
3659                // Special case: $$ alone (empty name) refers to parent/root context
3660                if name.is_empty() {
3661                    return self.context.get_parent().cloned().ok_or_else(|| {
3662                        EvaluatorError::ReferenceError("Parent context not available".to_string())
3663                    });
3664                }
3665
3666                // For $$name, we need to evaluate name against parent context
3667                // This is similar to $.name but using parent data
3668                let parent_data = self.context.get_parent().ok_or_else(|| {
3669                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3670                })?;
3671
3672                // Access field on parent context
3673                match parent_data {
3674                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3675                    _ => Ok(JValue::Null),
3676                }
3677            }
3678
3679            AstNode::Path { steps } => self.evaluate_path(steps, data),
3680
3681            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3682
3683            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3684
3685            // Array constructor - JSONata semantics:
3686            AstNode::Array(elements) => {
3687                // - If element is itself an array constructor [...], keep it nested
3688                // - Otherwise, if element evaluates to an array, flatten it
3689                // - Undefined values are excluded
3690                let mut result = Vec::with_capacity(elements.len());
3691                for element in elements {
3692                    // Check if this element is itself an explicit array constructor
3693                    let is_array_constructor = matches!(element, AstNode::Array(_));
3694
3695                    let value = self.evaluate_internal(element, data)?;
3696
3697                    // Skip undefined values in array constructors
3698                    // Note: explicit null is preserved, only undefined (no value) is filtered
3699                    if value.is_undefined() {
3700                        continue;
3701                    }
3702
3703                    if is_array_constructor {
3704                        // Explicit array constructor - keep nested
3705                        result.push(value);
3706                    } else if let JValue::Array(arr) = value {
3707                        // Non-array-constructor that evaluated to array - flatten it
3708                        result.extend(arr.iter().cloned());
3709                    } else {
3710                        // Non-array value - add as-is
3711                        result.push(value);
3712                    }
3713                }
3714                Ok(JValue::array(result))
3715            }
3716
3717            AstNode::Object(pairs) => {
3718                let mut result = IndexMap::with_capacity(pairs.len());
3719
3720                // Check if all keys are string literals — can skip D1009 HashMap
3721                let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3722
3723                if all_literal_keys {
3724                    // Fast path: literal keys, no need for D1009 tracking
3725                    for (key_node, value_node) in pairs.iter() {
3726                        let key = match key_node {
3727                            AstNode::String(s) => s,
3728                            _ => unreachable!(),
3729                        };
3730                        let value = self.evaluate_internal(value_node, data)?;
3731                        if value.is_undefined() {
3732                            continue;
3733                        }
3734                        result.insert(key.clone(), value);
3735                    }
3736                } else {
3737                    let mut key_sources: HashMap<String, usize> = HashMap::new();
3738                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3739                        let key = match self.evaluate_internal(key_node, data)? {
3740                            JValue::String(s) => s,
3741                            JValue::Null => continue,
3742                            other => {
3743                                if other.is_undefined() {
3744                                    continue;
3745                                }
3746                                return Err(EvaluatorError::TypeError(format!(
3747                                    "Object key must be a string, got: {:?}",
3748                                    other
3749                                )));
3750                            }
3751                        };
3752
3753                        if let Some(&existing_idx) = key_sources.get(&*key) {
3754                            if existing_idx != pair_index {
3755                                return Err(EvaluatorError::EvaluationError(format!(
3756                                    "D1009: Multiple key expressions evaluate to same key: {}",
3757                                    key
3758                                )));
3759                            }
3760                        }
3761                        key_sources.insert(key.to_string(), pair_index);
3762
3763                        let value = self.evaluate_internal(value_node, data)?;
3764                        if value.is_undefined() {
3765                            continue;
3766                        }
3767                        result.insert(key.to_string(), value);
3768                    }
3769                }
3770                Ok(JValue::object(result))
3771            }
3772
3773            // Object transform: group items by key, then evaluate value once per group
3774            AstNode::ObjectTransform { input, pattern } => {
3775                // Evaluate the input expression. Keep tuple wrappers alive so the
3776                // group-by key/value expressions can read the carried `$focus`
3777                // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3778                let saved_keep = self.keep_tuple_stream;
3779                self.keep_tuple_stream = true;
3780                let input_value = self.evaluate_internal(input, data);
3781                self.keep_tuple_stream = saved_keep;
3782                let input_value = input_value?;
3783
3784                // If input is undefined, return undefined (not empty object)
3785                if input_value.is_undefined() {
3786                    return Ok(JValue::Undefined);
3787                }
3788
3789                // Handle array input - process each item
3790                let items: Vec<JValue> = match input_value {
3791                    JValue::Array(ref arr) => (**arr).clone(),
3792                    JValue::Null => return Ok(JValue::Null),
3793                    other => vec![other],
3794                };
3795
3796                // If array is empty, add undefined to enable literal JSON object generation
3797                let items = if items.is_empty() {
3798                    vec![JValue::Undefined]
3799                } else {
3800                    items
3801                };
3802
3803                // Grouping over a tuple stream ("reduce" mode, mirroring
3804                // jsonata-js evaluateGroupExpression): each item is a
3805                // `{@, $var, !label, __tuple__}` wrapper. The key/value
3806                // expressions are evaluated against the tuple's `@` value with the
3807                // carried focus/index/ancestor keys bound into scope (so
3808                // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3809                // `$e` AND resolves the relative `Phone` against the Contact `@`),
3810                // and grouped tuples are reduced (per-key values appended) before
3811                // the value expression sees them.
3812                let reduce = items.first().is_some_and(|it| {
3813                    matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3814                });
3815
3816                // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3817                // returns the saved prior values so they can be restored.
3818                let bind_tuple = |ev: &mut Self,
3819                                  tuple: &IndexMap<String, JValue>|
3820                 -> Vec<(String, Option<JValue>)> {
3821                    let mut saved = Vec::new();
3822                    for (k, v) in tuple.iter() {
3823                        let name = if let Some(n) = k.strip_prefix('$') {
3824                            if n.is_empty() {
3825                                continue;
3826                            } else {
3827                                n.to_string()
3828                            }
3829                        } else if k.starts_with('!') {
3830                            k.clone()
3831                        } else {
3832                            continue;
3833                        };
3834                        saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3835                        ev.context.bind(name, v.clone());
3836                    }
3837                    saved
3838                };
3839                let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3840                    for (name, old) in saved.into_iter().rev() {
3841                        match old {
3842                            Some(v) => ev.context.bind(name, v),
3843                            None => ev.context.unbind(&name),
3844                        }
3845                    }
3846                };
3847
3848                // Phase 1: Group items by key expression
3849                // groups maps key -> (grouped_data, expr_index)
3850                // When multiple items have same key, their data is appended together
3851                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3852
3853                // Save the current $ binding to restore later
3854                let saved_dollar = self.context.lookup("$").cloned();
3855
3856                for item in &items {
3857                    // In reduce mode evaluate the key against `@` with tuple keys
3858                    // bound; otherwise against the item itself.
3859                    let (key_data, tuple_saved) = match (reduce, item) {
3860                        (true, JValue::Object(o)) => {
3861                            let saved = bind_tuple(self, o);
3862                            (
3863                                o.get("@").cloned().unwrap_or(JValue::Undefined),
3864                                Some(saved),
3865                            )
3866                        }
3867                        _ => (item.clone(), None),
3868                    };
3869                    self.context.bind("$".to_string(), key_data.clone());
3870
3871                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3872                        // Evaluate key with current item as context
3873                        let key = match self.evaluate_internal(key_node, &key_data)? {
3874                            JValue::String(s) => s,
3875                            JValue::Null => continue, // Skip null keys
3876                            other => {
3877                                // Skip undefined keys
3878                                if other.is_undefined() {
3879                                    continue;
3880                                }
3881                                if let Some(saved) = tuple_saved {
3882                                    restore(self, saved);
3883                                }
3884                                return Err(EvaluatorError::TypeError(format!(
3885                                    "T1003: Object key must be a string, got: {:?}",
3886                                    other
3887                                )));
3888                            }
3889                        };
3890
3891                        // Group items by key
3892                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3893                            // Key already exists - check if from same expression index
3894                            if *existing_idx != pair_index {
3895                                if let Some(saved) = tuple_saved {
3896                                    restore(self, saved);
3897                                }
3898                                // D1009: multiple key expressions evaluate to same key
3899                                return Err(EvaluatorError::EvaluationError(format!(
3900                                    "D1009: Multiple key expressions evaluate to same key: {}",
3901                                    key
3902                                )));
3903                            }
3904                            // Append item to the group
3905                            existing_data.push(item.clone());
3906                        } else {
3907                            // New key - create new group
3908                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3909                        }
3910                    }
3911
3912                    if let Some(saved) = tuple_saved {
3913                        restore(self, saved);
3914                    }
3915                }
3916
3917                // Phase 2: Evaluate value expression for each group
3918                let mut result = IndexMap::new();
3919
3920                for (key, (grouped_data, expr_index)) in groups {
3921                    // Get the value expression for this group
3922                    let (_key_node, value_node) = &pattern[expr_index];
3923
3924                    if reduce {
3925                        // Reduce the grouped tuples into one (per-key values
3926                        // appended), mirroring jsonata-js reduceTupleStream, then
3927                        // evaluate the value against the merged `@` with the merged
3928                        // focus/index/ancestor keys bound.
3929                        let merged = reduce_tuple_stream(&grouped_data);
3930                        let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3931                        let mut tuple_no_at = merged.clone();
3932                        tuple_no_at.shift_remove("@");
3933                        let saved = bind_tuple(self, &tuple_no_at);
3934                        self.context.bind("$".to_string(), context.clone());
3935                        let value = self.evaluate_internal(value_node, &context);
3936                        restore(self, saved);
3937                        let value = value?;
3938                        if !value.is_undefined() {
3939                            result.insert(key, value);
3940                        }
3941                        continue;
3942                    }
3943
3944                    // Determine the context for value evaluation:
3945                    // - If single item, use that item directly
3946                    // - If multiple items, use the array of items
3947                    let context = if grouped_data.len() == 1 {
3948                        grouped_data.into_iter().next().unwrap()
3949                    } else {
3950                        JValue::array(grouped_data)
3951                    };
3952
3953                    // Bind $ to the context for value evaluation
3954                    self.context.bind("$".to_string(), context.clone());
3955
3956                    // Evaluate value expression with grouped context
3957                    let value = self.evaluate_internal(value_node, &context)?;
3958
3959                    // Skip undefined values
3960                    if !value.is_undefined() {
3961                        result.insert(key, value);
3962                    }
3963                }
3964
3965                // Restore the previous $ binding
3966                if let Some(saved) = saved_dollar {
3967                    self.context.bind("$".to_string(), saved);
3968                } else {
3969                    self.context.unbind("$");
3970                }
3971
3972                Ok(JValue::object(result))
3973            }
3974
3975            AstNode::Function {
3976                name,
3977                args,
3978                is_builtin,
3979            } => self.evaluate_function_call(name, args, *is_builtin, data),
3980
3981            // Call: invoke an arbitrary expression as a function
3982            // Used for IIFE patterns like (function($x){...})(5) or chained calls
3983            AstNode::Call { procedure, args } => {
3984                // Evaluate the procedure to get the callable value
3985                let callable = self.evaluate_internal(procedure, data)?;
3986
3987                // Check if it's a lambda value
3988                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3989                    let mut evaluated_args = Vec::with_capacity(args.len());
3990                    for arg in args.iter() {
3991                        evaluated_args.push(self.evaluate_internal(arg, data)?);
3992                    }
3993                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3994                }
3995
3996                // Not a callable value
3997                Err(EvaluatorError::TypeError(format!(
3998                    "Cannot call non-function value: {:?}",
3999                    callable
4000                )))
4001            }
4002
4003            AstNode::Conditional {
4004                condition,
4005                then_branch,
4006                else_branch,
4007            } => {
4008                let condition_value = self.evaluate_internal(condition, data)?;
4009                if self.is_truthy(&condition_value) {
4010                    self.evaluate_internal(then_branch, data)
4011                } else if let Some(else_branch) = else_branch {
4012                    self.evaluate_internal(else_branch, data)
4013                } else {
4014                    // No else branch - return undefined (not null)
4015                    // This allows $map to filter out results from conditionals without else
4016                    Ok(JValue::Undefined)
4017                }
4018            }
4019
4020            AstNode::Block(expressions) => {
4021                // Blocks create a new scope - push scope instead of clone/restore
4022                self.context.push_scope();
4023
4024                let mut result = JValue::Null;
4025                for expr in expressions {
4026                    result = self.evaluate_internal(expr, data)?;
4027                }
4028
4029                // Before popping, preserve any lambdas referenced by the result
4030                // This is essential for closures returned from blocks (IIFE pattern)
4031                let lambdas_to_keep = self.extract_lambda_ids(&result);
4032                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
4033
4034                Ok(result)
4035            }
4036
4037            // Lambda: capture current environment for closure support
4038            AstNode::Lambda {
4039                params,
4040                body,
4041                signature,
4042                thunk,
4043            } => {
4044                let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
4045
4046                let compiled_body = if !thunk {
4047                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
4048                    try_compile_expr_with_allowed_vars(body, &var_refs)
4049                } else {
4050                    None
4051                };
4052                let stored_lambda = StoredLambda {
4053                    params: params.clone(),
4054                    body: (**body).clone(),
4055                    compiled_body,
4056                    signature: signature.clone(),
4057                    captured_env: self.capture_environment_for(body, params),
4058                    captured_data: Some(data.clone()),
4059                    thunk: *thunk,
4060                };
4061                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
4062
4063                let lambda_obj = JValue::lambda(
4064                    lambda_id.as_str(),
4065                    params.clone(),
4066                    None::<String>,
4067                    signature.clone(),
4068                );
4069
4070                Ok(lambda_obj)
4071            }
4072
4073            // Wildcard: collect all values from current object
4074            AstNode::Wildcard => {
4075                let normalized = normalize_lazy(data)?;
4076                match &normalized {
4077                    JValue::Object(obj) => {
4078                        let mut result = Vec::new();
4079                        for value in obj.values() {
4080                            // Flatten arrays into the result
4081                            match value {
4082                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
4083                                _ => result.push(value.clone()),
4084                            }
4085                        }
4086                        check_sequence_length(result.len(), &self.options)?;
4087                        Ok(JValue::array(result))
4088                    }
4089                    JValue::Array(arr) => {
4090                        // For arrays, wildcard returns all elements
4091                        Ok(JValue::Array(arr.clone()))
4092                    }
4093                    _ => Ok(JValue::Null),
4094                }
4095            }
4096
4097            // Descendant: recursively traverse all nested values
4098            AstNode::Descendant => {
4099                let descendants = self.collect_descendants(data)?;
4100                if descendants.is_empty() {
4101                    Ok(JValue::Null) // No descendants means undefined
4102                } else {
4103                    check_sequence_length(descendants.len(), &self.options)?;
4104                    Ok(JValue::array(descendants))
4105                }
4106            }
4107
4108            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
4109                "Predicate can only be used in path expressions".to_string(),
4110            )),
4111
4112            // Array grouping: same as Array but prevents flattening in path contexts
4113            AstNode::ArrayGroup(elements) => {
4114                let mut result = Vec::new();
4115                for element in elements {
4116                    let value = self.evaluate_internal(element, data)?;
4117                    result.push(value);
4118                }
4119                Ok(JValue::array(result))
4120            }
4121
4122            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
4123                "Function application can only be used in path expressions".to_string(),
4124            )),
4125
4126            AstNode::Sort { input, terms } => {
4127                // Keep the input path's tuple wrappers so the sort terms can read
4128                // the carried `%`/`$focus`/`$index` bindings per element.
4129                let saved = self.keep_tuple_stream;
4130                self.keep_tuple_stream = true;
4131                let value = self.evaluate_internal(input, data);
4132                self.keep_tuple_stream = saved;
4133                self.evaluate_sort(&value?, terms)
4134            }
4135
4136            // Transform: |location|update[,delete]|
4137            AstNode::Transform {
4138                location,
4139                update,
4140                delete,
4141            } => {
4142                // Check if $ is bound (meaning we're being invoked as a lambda)
4143                if self.context.lookup("$").is_some() {
4144                    // Execute the transformation
4145                    self.execute_transform(location, update, delete.as_deref(), data)
4146                } else {
4147                    // Return a lambda representation
4148                    // The transform will be executed when the lambda is invoked
4149                    let transform_lambda = StoredLambda {
4150                        params: vec!["$".to_string()],
4151                        body: AstNode::Transform {
4152                            location: location.clone(),
4153                            update: update.clone(),
4154                            delete: delete.clone(),
4155                        },
4156                        compiled_body: None, // Transform is not a pure compilable expr
4157                        signature: None,
4158                        captured_env: HashMap::new(),
4159                        captured_data: None, // Transform takes $ as parameter
4160                        thunk: false,
4161                    };
4162
4163                    // Store with a generated unique name
4164                    let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
4165                    self.context.bind_lambda(lambda_name, transform_lambda);
4166
4167                    // Return lambda marker
4168                    Ok(JValue::string("<lambda>"))
4169                }
4170            }
4171
4172            // Parent-reference operator (%): ast_transform has already resolved
4173            // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
4174            // tuple step binds that label into scope (create_tuple_stream +
4175            // needs_tuple_context_binding), so resolving it is an ordinary scope
4176            // lookup, mirroring jsonata-js's
4177            // `case 'parent': result = environment.lookup(expr.slot.label);`.
4178            AstNode::Parent(label) => {
4179                if let Some(v) = self.context.lookup(label) {
4180                    return Ok(v.clone());
4181                }
4182                // Fall back to the tuple wrapper carried as `data`: a `%` used
4183                // inside a predicate/stage over a tuple stream -- e.g.
4184                // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
4185                // predicate is evaluated per tuple with the wrapper as data --
4186                // reads its ancestor from the tuple's `!label` key, which isn't
4187                // separately bound into scope here (mirrors AstNode::Variable's
4188                // tuple-binding fallback below).
4189                if let JValue::Object(obj) = data {
4190                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4191                        if let Some(v) = obj.get(label) {
4192                            return Ok(v.clone());
4193                        }
4194                    }
4195                }
4196                Ok(JValue::Undefined)
4197            }
4198        }
4199    }
4200
4201    /// Apply stages (filters/predicates) to a value during field extraction
4202    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
4203    /// This matches the JavaScript reference where stages apply to sequences
4204    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
4205        // Wrap non-arrays in an array for filtering (JSONata semantics)
4206        let mut result = match value {
4207            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
4208            JValue::Array(_) => value,
4209            other => JValue::array(vec![other]),
4210        };
4211
4212        for stage in stages {
4213            match stage {
4214                Stage::Filter(predicate_expr) => {
4215                    // When applying stages, use stage-specific predicate logic
4216                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
4217                }
4218                // Positional index stages are meaningful only over a tuple stream
4219                // (they set a variable to each tuple's position); they are applied
4220                // in `create_tuple_stream`, not on a plain value sequence here.
4221                Stage::Index(_) => {}
4222            }
4223        }
4224        Ok(result)
4225    }
4226
4227    /// Check if an AST node is definitely a filter expression (comparison/logical)
4228    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
4229    fn is_filter_predicate(predicate: &AstNode) -> bool {
4230        match predicate {
4231            AstNode::Binary { op, .. } => matches!(
4232                op,
4233                BinaryOp::GreaterThan
4234                    | BinaryOp::GreaterThanOrEqual
4235                    | BinaryOp::LessThan
4236                    | BinaryOp::LessThanOrEqual
4237                    | BinaryOp::Equal
4238                    | BinaryOp::NotEqual
4239                    | BinaryOp::And
4240                    | BinaryOp::Or
4241                    | BinaryOp::In
4242            ),
4243            AstNode::Unary {
4244                op: crate::ast::UnaryOp::Not,
4245                ..
4246            } => true,
4247            _ => false,
4248        }
4249    }
4250
4251    /// Evaluate a predicate as a stage during field extraction
4252    /// This has different semantics than standalone predicates:
4253    /// - Maps index operations over arrays of extracted values
4254    fn evaluate_predicate_as_stage(
4255        &mut self,
4256        current: &JValue,
4257        predicate: &AstNode,
4258    ) -> Result<JValue, EvaluatorError> {
4259        // Special case: empty brackets [] (represented as Boolean(true))
4260        if matches!(predicate, AstNode::Boolean(true)) {
4261            return match current {
4262                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
4263                JValue::Null => Ok(JValue::Null),
4264                other => Ok(JValue::array(vec![other.clone()])),
4265            };
4266        }
4267
4268        match current {
4269            JValue::Array(arr) => {
4270                // For stages: if we have an array of values (from field extraction),
4271                // apply the predicate to each value if appropriate
4272
4273                // Check if predicate is a numeric index
4274                if let AstNode::Number(n) = predicate {
4275                    // Check if this is an array of arrays (extracted array fields)
4276                    let is_array_of_arrays =
4277                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
4278
4279                    if !is_array_of_arrays {
4280                        // Simple values: just index normally
4281                        return self.array_index(current, &JValue::Number(*n));
4282                    }
4283
4284                    // Array of arrays: map index access over each extracted array
4285                    let mut result = Vec::new();
4286                    for item in arr.iter() {
4287                        match item {
4288                            JValue::Array(_) => {
4289                                let indexed = self.array_index(item, &JValue::Number(*n))?;
4290                                if !indexed.is_null() && !indexed.is_undefined() {
4291                                    result.push(indexed);
4292                                }
4293                            }
4294                            _ => {
4295                                if *n == 0.0 {
4296                                    result.push(item.clone());
4297                                }
4298                            }
4299                        }
4300                    }
4301                    return Ok(JValue::array(result));
4302                }
4303
4304                // Short-circuit: if predicate is definitely a comparison/logical expression,
4305                // skip speculative numeric evaluation and go directly to filter logic
4306                if Self::is_filter_predicate(predicate) {
4307                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
4308                    if let Some(compiled) = try_compile_expr(predicate) {
4309                        let shape = arr.first().and_then(build_shape_cache);
4310                        let mut filtered = Vec::with_capacity(arr.len());
4311                        for item in arr.iter() {
4312                            let result = if let Some(ref s) = shape {
4313                                eval_compiled_shaped(
4314                                    &compiled,
4315                                    item,
4316                                    None,
4317                                    s,
4318                                    &self.options,
4319                                    self.start_time,
4320                                )?
4321                            } else {
4322                                eval_compiled(
4323                                    &compiled,
4324                                    item,
4325                                    None,
4326                                    &self.options,
4327                                    self.start_time,
4328                                )?
4329                            };
4330                            if compiled_is_truthy(&result) {
4331                                filtered.push(item.clone());
4332                            }
4333                        }
4334                        return Ok(JValue::array(filtered));
4335                    }
4336                    // Fallback: full AST evaluation
4337                    let mut filtered = Vec::new();
4338                    for item in arr.iter() {
4339                        let item_result = self.evaluate_internal(predicate, item)?;
4340                        if self.is_truthy(&item_result) {
4341                            filtered.push(item.clone());
4342                        }
4343                    }
4344                    return Ok(JValue::array(filtered));
4345                }
4346
4347                // Try to evaluate the predicate to see if it's a numeric index or array of indices
4348                // If evaluation succeeds and yields a number, use it as an index
4349                // If it yields an array of numbers, use them as multiple indices
4350                // If evaluation fails (e.g., comparison error), treat as filter
4351                match self.evaluate_internal(predicate, current) {
4352                    Ok(JValue::Number(n)) => {
4353                        let n_val = n;
4354                        let is_array_of_arrays =
4355                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
4356
4357                        if !is_array_of_arrays {
4358                            let pred_result = JValue::Number(n_val);
4359                            return self.array_index(current, &pred_result);
4360                        }
4361
4362                        // Array of arrays: map index access
4363                        let mut result = Vec::new();
4364                        let pred_result = JValue::Number(n_val);
4365                        for item in arr.iter() {
4366                            match item {
4367                                JValue::Array(_) => {
4368                                    let indexed = self.array_index(item, &pred_result)?;
4369                                    if !indexed.is_null() && !indexed.is_undefined() {
4370                                        result.push(indexed);
4371                                    }
4372                                }
4373                                _ => {
4374                                    if n_val == 0.0 {
4375                                        result.push(item.clone());
4376                                    }
4377                                }
4378                            }
4379                        }
4380                        return Ok(JValue::array(result));
4381                    }
4382                    Ok(JValue::Array(indices)) => {
4383                        // Array of values - could be indices or filter results
4384                        // Check if all values are numeric
4385                        let has_non_numeric =
4386                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
4387
4388                        if has_non_numeric {
4389                            // Non-numeric values - treat as filter, fall through
4390                        } else {
4391                            // All numeric - use as indices
4392                            let arr_len = arr.len() as i64;
4393                            let mut resolved_indices: Vec<i64> = indices
4394                                .iter()
4395                                .filter_map(|v| {
4396                                    if let JValue::Number(n) = v {
4397                                        let idx = *n as i64;
4398                                        // Resolve negative indices
4399                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
4400                                        // Only include valid indices
4401                                        if actual_idx >= 0 && actual_idx < arr_len {
4402                                            Some(actual_idx)
4403                                        } else {
4404                                            None
4405                                        }
4406                                    } else {
4407                                        None
4408                                    }
4409                                })
4410                                .collect();
4411
4412                            // Sort and deduplicate indices
4413                            resolved_indices.sort();
4414                            resolved_indices.dedup();
4415
4416                            // Select elements at each sorted index
4417                            let result: Vec<JValue> = resolved_indices
4418                                .iter()
4419                                .map(|&idx| arr[idx as usize].clone())
4420                                .collect();
4421
4422                            return Ok(JValue::array(result));
4423                        }
4424                    }
4425                    Ok(_) => {
4426                        // Evaluated successfully but not a number or array - might be a filter
4427                        // Fall through to filter logic
4428                    }
4429                    Err(_) => {
4430                        // Evaluation failed - it's likely a filter expression
4431                        // Fall through to filter logic
4432                    }
4433                }
4434
4435                // It's a filter expression
4436                let mut filtered = Vec::new();
4437                for item in arr.iter() {
4438                    let item_result = self.evaluate_internal(predicate, item)?;
4439                    if self.is_truthy(&item_result) {
4440                        filtered.push(item.clone());
4441                    }
4442                }
4443                Ok(JValue::array(filtered))
4444            }
4445            JValue::Null => {
4446                // Null: return null
4447                Ok(JValue::Null)
4448            }
4449            other => {
4450                // Non-array values: treat as single-element conceptual array
4451                // For numeric predicates: index 0 returns the value, other indices return null
4452                // For boolean predicates: if truthy, return value; if falsy, return null
4453
4454                // Check if predicate is a numeric index
4455                if let AstNode::Number(n) = predicate {
4456                    // Index 0 returns the value, other indices return null
4457                    if *n == 0.0 {
4458                        return Ok(other.clone());
4459                    } else {
4460                        return Ok(JValue::Null);
4461                    }
4462                }
4463
4464                // Try to evaluate the predicate to see if it's a numeric index
4465                match self.evaluate_internal(predicate, other) {
4466                    Ok(JValue::Number(n)) => {
4467                        // Index 0 returns the value, other indices return null
4468                        if n == 0.0 {
4469                            Ok(other.clone())
4470                        } else {
4471                            Ok(JValue::Null)
4472                        }
4473                    }
4474                    Ok(pred_result) => {
4475                        // Boolean filter: return value if truthy, null if falsy
4476                        if self.is_truthy(&pred_result) {
4477                            Ok(other.clone())
4478                        } else {
4479                            Ok(JValue::Null)
4480                        }
4481                    }
4482                    Err(e) => Err(e),
4483                }
4484            }
4485        }
4486    }
4487
4488    /// Evaluate a path expression (e.g., foo.bar.baz)
4489    fn evaluate_path(
4490        &mut self,
4491        steps: &[PathStep],
4492        data: &JValue,
4493    ) -> Result<JValue, EvaluatorError> {
4494        // Avoid cloning by using references and only cloning when necessary
4495        if steps.is_empty() {
4496            return Ok(data.clone());
4497        }
4498
4499        // Fast path: single field access on object
4500        // This is a very common pattern, so optimize it.
4501        // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
4502        // creation handled below.
4503        if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
4504            if let AstNode::Name(field_name) = &steps[0].node {
4505                return match data {
4506                    JValue::Object(obj) => {
4507                        // Check if this is a tuple - extract '@' value
4508                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4509                            match obj.get("@") {
4510                                Some(JValue::Object(inner)) => {
4511                                    Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
4512                                }
4513                                #[cfg(feature = "python")]
4514                                Some(JValue::LazyPyDict(lazy)) => Ok(lazy.get_field(field_name)?),
4515                                _ => Ok(JValue::Undefined),
4516                            }
4517                        } else {
4518                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
4519                        }
4520                    }
4521                    #[cfg(feature = "python")]
4522                    JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field_name)?),
4523                    JValue::Array(arr) => {
4524                        // Array mapping: extract field from each element
4525                        // Optimized: use references to access fields without cloning entire objects
4526                        // Check first element for tuple-ness (tuples are all-or-nothing)
4527                        let has_tuples = arr.first().is_some_and(|item| {
4528                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4529                        });
4530
4531                        if !has_tuples {
4532                            // Fast path: no tuples, just direct field lookups
4533                            let mut result = Vec::with_capacity(arr.len());
4534                            for item in arr.iter() {
4535                                if let JValue::Object(obj) = item {
4536                                    if let Some(val) = obj.get(field_name) {
4537                                        if !val.is_null() {
4538                                            match val {
4539                                                JValue::Array(arr_val) => {
4540                                                    result.extend(arr_val.iter().cloned());
4541                                                }
4542                                                other => result.push(other.clone()),
4543                                            }
4544                                        }
4545                                    }
4546                                } else if let JValue::Array(inner_arr) = item {
4547                                    let nested_result = self.evaluate_path(
4548                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
4549                                        &JValue::Array(inner_arr.clone()),
4550                                    )?;
4551                                    match nested_result {
4552                                        JValue::Array(nested) => {
4553                                            result.extend(nested.iter().cloned());
4554                                        }
4555                                        JValue::Null => {}
4556                                        other => result.push(other),
4557                                    }
4558                                } else {
4559                                    #[cfg(feature = "python")]
4560                                    if let JValue::LazyPyDict(lazy) = item {
4561                                        let val = lazy.get_field(field_name)?;
4562                                        if !val.is_null() && !val.is_undefined() {
4563                                            match val {
4564                                                JValue::Array(arr_val) => {
4565                                                    result.extend(arr_val.iter().cloned());
4566                                                }
4567                                                other => result.push(other),
4568                                            }
4569                                        }
4570                                    }
4571                                }
4572                            }
4573
4574                            if result.is_empty() {
4575                                Ok(JValue::Null)
4576                            } else if result.len() == 1 {
4577                                Ok(result.into_iter().next().unwrap())
4578                            } else {
4579                                check_sequence_length(result.len(), &self.options)?;
4580                                Ok(JValue::array(result))
4581                            }
4582                        } else {
4583                            // Tuple path: per-element tuple handling
4584                            let mut result = Vec::new();
4585                            for item in arr.iter() {
4586                                match item {
4587                                    JValue::Object(obj) => {
4588                                        let is_tuple =
4589                                            obj.get("__tuple__") == Some(&JValue::Bool(true));
4590
4591                                        if is_tuple {
4592                                            let field_val: Option<JValue> = match obj.get("@") {
4593                                                Some(JValue::Object(inner)) => {
4594                                                    inner.get(field_name).cloned()
4595                                                }
4596                                                #[cfg(feature = "python")]
4597                                                Some(JValue::LazyPyDict(lazy)) => {
4598                                                    let v = lazy.get_field(field_name)?;
4599                                                    if v.is_undefined() {
4600                                                        None
4601                                                    } else {
4602                                                        Some(v)
4603                                                    }
4604                                                }
4605                                                _ => continue,
4606                                            };
4607
4608                                            if let Some(val) = field_val.as_ref() {
4609                                                if !val.is_null() {
4610                                                    // Build tuple wrapper - only clone bindings when needed
4611                                                    let wrap = |v: JValue| -> JValue {
4612                                                        let mut wrapper = IndexMap::new();
4613                                                        wrapper.insert("@".to_string(), v);
4614                                                        wrapper.insert(
4615                                                            "__tuple__".to_string(),
4616                                                            JValue::Bool(true),
4617                                                        );
4618                                                        for (k, v) in obj.iter() {
4619                                                            if k.starts_with('$') {
4620                                                                wrapper
4621                                                                    .insert(k.clone(), v.clone());
4622                                                            }
4623                                                        }
4624                                                        JValue::object(wrapper)
4625                                                    };
4626
4627                                                    match val {
4628                                                        JValue::Array(arr_val) => {
4629                                                            for item in arr_val.iter() {
4630                                                                result.push(wrap(item.clone()));
4631                                                            }
4632                                                        }
4633                                                        other => result.push(wrap(other.clone())),
4634                                                    }
4635                                                }
4636                                            }
4637                                        } else {
4638                                            // Non-tuple: access field directly by reference, only clone the field value
4639                                            if let Some(val) = obj.get(field_name) {
4640                                                if !val.is_null() {
4641                                                    match val {
4642                                                        JValue::Array(arr_val) => {
4643                                                            for item in arr_val.iter() {
4644                                                                result.push(item.clone());
4645                                                            }
4646                                                        }
4647                                                        other => result.push(other.clone()),
4648                                                    }
4649                                                }
4650                                            }
4651                                        }
4652                                    }
4653                                    JValue::Array(inner_arr) => {
4654                                        // Recursively map over nested array
4655                                        let nested_result = self.evaluate_path(
4656                                            &[PathStep::new(AstNode::Name(field_name.clone()))],
4657                                            &JValue::Array(inner_arr.clone()),
4658                                        )?;
4659                                        // Add nested result to our results
4660                                        match nested_result {
4661                                            JValue::Array(nested) => {
4662                                                // Flatten nested arrays from recursive mapping
4663                                                result.extend(nested.iter().cloned());
4664                                            }
4665                                            JValue::Null => {} // Skip nulls from nested arrays
4666                                            other => result.push(other),
4667                                        }
4668                                    }
4669                                    _ => {} // Skip non-object items
4670                                }
4671                            }
4672
4673                            // Return array result
4674                            // JSONata singleton unwrapping: if we have exactly one result,
4675                            // unwrap it (even if it's an array)
4676                            if result.is_empty() {
4677                                Ok(JValue::Null)
4678                            } else if result.len() == 1 {
4679                                Ok(result.into_iter().next().unwrap())
4680                            } else {
4681                                check_sequence_length(result.len(), &self.options)?;
4682                                Ok(JValue::array(result))
4683                            }
4684                        } // end else (tuple path)
4685                    }
4686                    _ => Ok(JValue::Undefined),
4687                };
4688            }
4689        }
4690
4691        // Fast path: 2-step $variable.field with no stages
4692        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4693        if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4694            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4695                (&steps[0].node, &steps[1].node)
4696            {
4697                if !var_name.is_empty() {
4698                    if let Some(value) = self.context.lookup(var_name) {
4699                        match value {
4700                            JValue::Object(obj) => {
4701                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4702                            }
4703                            #[cfg(feature = "python")]
4704                            JValue::LazyPyDict(lazy) => {
4705                                let v = lazy.get_field(field_name)?;
4706                                return Ok(if v.is_undefined() { JValue::Null } else { v });
4707                            }
4708                            JValue::Array(arr) => {
4709                                // Map field extraction over array (same as single-step Name on Array)
4710                                let mut result = Vec::with_capacity(arr.len());
4711                                for item in arr.iter() {
4712                                    if let JValue::Object(obj) = item {
4713                                        if let Some(val) = obj.get(field_name) {
4714                                            if !val.is_null() {
4715                                                match val {
4716                                                    JValue::Array(inner) => {
4717                                                        result.extend(inner.iter().cloned());
4718                                                    }
4719                                                    other => result.push(other.clone()),
4720                                                }
4721                                            }
4722                                        }
4723                                    } else {
4724                                        #[cfg(feature = "python")]
4725                                        if let JValue::LazyPyDict(lazy) = item {
4726                                            let val = lazy.get_field(field_name)?;
4727                                            if !val.is_null() && !val.is_undefined() {
4728                                                match val {
4729                                                    JValue::Array(inner) => {
4730                                                        result.extend(inner.iter().cloned());
4731                                                    }
4732                                                    other => result.push(other),
4733                                                }
4734                                            }
4735                                        }
4736                                    }
4737                                }
4738                                return match result.len() {
4739                                    0 => Ok(JValue::Null),
4740                                    1 => Ok(result.pop().unwrap()),
4741                                    _ => {
4742                                        check_sequence_length(result.len(), &self.options)?;
4743                                        Ok(JValue::array(result))
4744                                    }
4745                                };
4746                            }
4747                            _ => {} // Fall through to general path evaluation
4748                        }
4749                    }
4750                }
4751            }
4752        }
4753
4754        // Track whether we did array mapping (for singleton unwrapping)
4755        let mut did_array_mapping = false;
4756
4757        // For the first step, work with a reference.
4758        // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4759        // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4760        // first path step where tupleBindings is undefined.
4761        let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4762            JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4763        } else {
4764            match &steps[0].node {
4765                AstNode::Wildcard => {
4766                    // Wildcard as first step
4767                    let normalized = normalize_lazy(data)?;
4768                    match &normalized {
4769                        JValue::Object(obj) => {
4770                            let mut result = Vec::new();
4771                            for value in obj.values() {
4772                                // Flatten arrays into the result
4773                                match value {
4774                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4775                                    _ => result.push(value.clone()),
4776                                }
4777                            }
4778                            JValue::array(result)
4779                        }
4780                        JValue::Array(arr) => JValue::Array(arr.clone()),
4781                        _ => JValue::Null,
4782                    }
4783                }
4784                AstNode::Descendant => {
4785                    // Descendant as first step
4786                    let descendants = self.collect_descendants(data)?;
4787                    JValue::array(descendants)
4788                }
4789                AstNode::ParentVariable(name) => {
4790                    // Parent variable as first step
4791                    let parent_data = self.context.get_parent().ok_or_else(|| {
4792                        EvaluatorError::ReferenceError("Parent context not available".to_string())
4793                    })?;
4794
4795                    if name.is_empty() {
4796                        // $$ alone returns parent context
4797                        parent_data.clone()
4798                    } else {
4799                        // $$field accesses field on parent
4800                        match parent_data {
4801                            JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4802                            _ => JValue::Null,
4803                        }
4804                    }
4805                }
4806                AstNode::Name(field_name) => {
4807                    // Field/property access - get the stages for this step
4808                    let stages = &steps[0].stages;
4809
4810                    match data {
4811                        JValue::Object(obj) => {
4812                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4813                            // Apply any stages to the extracted value
4814                            if !stages.is_empty() {
4815                                self.apply_stages(val, stages)?
4816                            } else {
4817                                val
4818                            }
4819                        }
4820                        #[cfg(feature = "python")]
4821                        JValue::LazyPyDict(lazy) => {
4822                            let val = lazy.get_field(field_name)?;
4823                            if !stages.is_empty() {
4824                                self.apply_stages(val, stages)?
4825                            } else {
4826                                val
4827                            }
4828                        }
4829                        JValue::Array(arr) => {
4830                            // Array mapping: extract field from each element and apply stages
4831                            let mut result = Vec::new();
4832                            for item in arr.iter() {
4833                                match item {
4834                                    JValue::Object(obj) => {
4835                                        let val = obj
4836                                            .get(field_name)
4837                                            .cloned()
4838                                            .unwrap_or(JValue::Undefined);
4839                                        if !val.is_null() && !val.is_undefined() {
4840                                            if !stages.is_empty() {
4841                                                // Apply stages to the extracted value
4842                                                let processed_val =
4843                                                    self.apply_stages(val, stages)?;
4844                                                // Stages always return an array (or null); extend results
4845                                                match processed_val {
4846                                                    JValue::Array(arr) => {
4847                                                        result.extend(arr.iter().cloned())
4848                                                    }
4849                                                    JValue::Null => {} // Skip nulls from stage application
4850                                                    other => result.push(other), // Shouldn't happen, but handle it
4851                                                }
4852                                            } else {
4853                                                // No stages: flatten arrays, push scalars
4854                                                match val {
4855                                                    JValue::Array(arr) => {
4856                                                        result.extend(arr.iter().cloned())
4857                                                    }
4858                                                    other => result.push(other),
4859                                                }
4860                                            }
4861                                        }
4862                                    }
4863                                    JValue::Array(inner_arr) => {
4864                                        // Recursively map over nested array
4865                                        let nested_result = self.evaluate_path(
4866                                            &[steps[0].clone()],
4867                                            &JValue::Array(inner_arr.clone()),
4868                                        )?;
4869                                        match nested_result {
4870                                            JValue::Array(nested) => {
4871                                                result.extend(nested.iter().cloned())
4872                                            }
4873                                            JValue::Null => {} // Skip nulls from nested arrays
4874                                            other => result.push(other),
4875                                        }
4876                                    }
4877                                    #[cfg(feature = "python")]
4878                                    JValue::LazyPyDict(lazy) => {
4879                                        let val = lazy.get_field(field_name)?;
4880                                        if !val.is_null() && !val.is_undefined() {
4881                                            if !stages.is_empty() {
4882                                                let processed_val =
4883                                                    self.apply_stages(val, stages)?;
4884                                                match processed_val {
4885                                                    JValue::Array(arr) => {
4886                                                        result.extend(arr.iter().cloned())
4887                                                    }
4888                                                    JValue::Null => {}
4889                                                    other => result.push(other),
4890                                                }
4891                                            } else {
4892                                                match val {
4893                                                    JValue::Array(arr) => {
4894                                                        result.extend(arr.iter().cloned())
4895                                                    }
4896                                                    other => result.push(other),
4897                                                }
4898                                            }
4899                                        }
4900                                    }
4901                                    _ => {} // Skip non-object items
4902                                }
4903                            }
4904                            JValue::array(result)
4905                        }
4906                        JValue::Null => JValue::Null,
4907                        // Accessing field on non-object returns undefined (not an error)
4908                        _ => JValue::Undefined,
4909                    }
4910                }
4911                AstNode::String(string_literal) => {
4912                    // String literal in path context - evaluate as literal and apply stages
4913                    // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4914                    let stages = &steps[0].stages;
4915                    let val = JValue::string(string_literal.clone());
4916
4917                    if !stages.is_empty() {
4918                        // Apply stages (predicates) to the string literal
4919                        let result = self.apply_stages(val, stages)?;
4920                        // Unwrap single-element arrays back to scalar
4921                        // (string literals with predicates should return scalar or null, not arrays)
4922                        match result {
4923                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4924                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4925                            other => other,
4926                        }
4927                    } else {
4928                        val
4929                    }
4930                }
4931                AstNode::Predicate(pred_expr) => {
4932                    // Predicate as first step
4933                    self.evaluate_predicate(data, pred_expr)?
4934                }
4935                _ => {
4936                    // Complex first step - evaluate it. When the step is
4937                    // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4938                    // whose `Product` is `%`-tagged, as in
4939                    // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4940                    // inner path's tuple wrappers so the following predicate/step
4941                    // can read the `!label` bindings.
4942                    let saved_keep = self.keep_tuple_stream;
4943                    if steps[0].is_tuple {
4944                        self.keep_tuple_stream = true;
4945                    }
4946                    let v = self.evaluate_path_step(&steps[0].node, data, data);
4947                    self.keep_tuple_stream = saved_keep;
4948                    v?
4949                }
4950            }
4951        };
4952
4953        // Process remaining steps
4954        for (step_idx, step) in steps[1..].iter().enumerate() {
4955            let is_last_step = step_idx == steps.len() - 2;
4956            // Early return if current is null/undefined - no point continuing
4957            // This handles cases like `blah.{}` where blah doesn't exist
4958            if current.is_null() {
4959                return Ok(JValue::Null);
4960            }
4961            if current.is_undefined() {
4962                return Ok(JValue::Undefined);
4963            }
4964
4965            // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4966            // a tuple stream, which selects a single tuple and unwraps it out of
4967            // the array) must stay a tuple stream so the following step keeps
4968            // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4969            // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4970            if let JValue::Object(o) = &current {
4971                if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4972                    current = JValue::array(vec![current.clone()]);
4973                    // The lone wrapper came from a singleton index selection, so
4974                    // the final result should unwrap back to a scalar (a following
4975                    // object step must not leave a spurious 1-element array).
4976                    did_array_mapping = true;
4977                }
4978            }
4979
4980            // Check if current is a tuple array - if so, we need to bind tuple variables
4981            // to context so they're available in nested expressions (like predicates)
4982            let is_tuple_array = if let JValue::Array(arr) = &current {
4983                arr.first().is_some_and(|first| {
4984                    if let JValue::Object(obj) = first {
4985                        obj.get("__tuple__") == Some(&JValue::Bool(true))
4986                    } else {
4987                        false
4988                    }
4989                })
4990            } else {
4991                false
4992            };
4993
4994            // Tuple-binding step (@ focus / # index / % parent): create/extend the
4995            // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4996            // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4997            // wrappers via the existing tuple-aware handling below.
4998            //
4999            // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
5000            // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
5001            // stream, but ONLY when it is consuming an existing tuple stream:
5002            // its ancestor label lives in those incoming tuples, so
5003            // create_tuple_stream's per-tuple frame binding is what lets
5004            // `evaluate_internal(Parent, ..)` resolve it (and any predicate
5005            // stage on the `%` step then resolves in the same frame). A `%`
5006            // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
5007            // predicate, whose input is plain data, not a tuple stream) must
5008            // NOT be routed here -- it's an ordinary scope lookup.
5009            let is_parent_step_over_tuple =
5010                matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
5011            if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
5012                current = JValue::array(self.create_tuple_stream(step, &current, false)?);
5013                continue;
5014            }
5015
5016            // For tuple arrays with certain step types, we need special handling to bind
5017            // tuple variables to context so they're available in nested expressions.
5018            // This is needed for:
5019            // - Object constructors: {"label": $$.items[$i]} needs $i in context
5020            // - Function applications: .($$.items[$i]) needs $i in context
5021            // - Variable lookups: .$i needs to find the tuple binding
5022            //
5023            // Steps like Name (field access) already have proper tuple handling in their
5024            // specific cases, so we don't intercept those here.
5025            let needs_tuple_context_binding = is_tuple_array
5026                && matches!(
5027                    &step.node,
5028                    AstNode::Object(_)
5029                        | AstNode::FunctionApplication(_)
5030                        | AstNode::Variable(_)
5031                        | AstNode::ArrayGroup(_)
5032                );
5033
5034            if needs_tuple_context_binding {
5035                if let JValue::Array(arr) = &current {
5036                    let mut results = Vec::new();
5037
5038                    for tuple in arr.iter() {
5039                        if let JValue::Object(tuple_obj) = tuple {
5040                            // Extract tuple bindings so nested expressions can see
5041                            // them: `$var` focus/index bindings (stored `$name`,
5042                            // bound as `name`) AND `!label` ancestor bindings for
5043                            // `%` (stored and bound under the full `!label` key).
5044                            // Saves/restores rather than blindly unbinding, so a
5045                            // tuple key that collides with a live outer `:=`
5046                            // binding doesn't get deleted afterward.
5047                            let tuple_bindings = self.bind_tuple_keys(tuple_obj);
5048
5049                            // Get the actual value from the tuple (@ field)
5050                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
5051
5052                            // Evaluate the step
5053                            let step_result = match &step.node {
5054                                AstNode::Variable(_) => {
5055                                    // Variable lookup - check context (which now has bindings)
5056                                    self.evaluate_internal(&step.node, tuple)?
5057                                }
5058                                AstNode::Object(_) | AstNode::ArrayGroup(_) => {
5059                                    // Object / array constructor step (e.g.
5060                                    // `Product.[`Product Name`, %.OrderID]`) -
5061                                    // evaluate on the tuple's `@` value with the
5062                                    // carried `!label`/`$focus` bindings in scope
5063                                    // so an embedded `%` resolves.
5064                                    self.evaluate_internal(&step.node, &actual_data)?
5065                                }
5066                                AstNode::FunctionApplication(inner) => {
5067                                    // A parenthesized step `(expr)` consuming a tuple stream
5068                                    // (e.g. `Account.Order.Product.( %.OrderID )` or
5069                                    // `Employee@$e.(Contact)[...]`): evaluate the INNER
5070                                    // expression on the tuple's `@` value with `$` bound to
5071                                    // it, mirroring the non-tuple FunctionApplication step
5072                                    // handling. Routing the wrapper node itself through
5073                                    // evaluate_internal raises "Function application can only
5074                                    // be used in path expressions".
5075                                    let saved_dollar = self.context.lookup("$").cloned();
5076                                    self.context.bind("$".to_string(), actual_data.clone());
5077                                    // Keep tuple wrappers from the inner path alive:
5078                                    // when `inner` is itself a tuple-carrying path
5079                                    // (e.g. `(Order.Product)` whose `Product` is
5080                                    // `%`-tagged), its `!label` wrappers must survive
5081                                    // to be merged into this tuple by the rewrap below
5082                                    // (they feed a later `%`/`%.%`). Without this the
5083                                    // inner path projects to `@` and drops the labels.
5084                                    let saved_keep = self.keep_tuple_stream;
5085                                    self.keep_tuple_stream = true;
5086                                    let v = self.evaluate_internal(inner, &actual_data);
5087                                    self.keep_tuple_stream = saved_keep;
5088                                    match saved_dollar {
5089                                        Some(s) => self.context.bind("$".to_string(), s),
5090                                        None => self.context.unbind("$"),
5091                                    }
5092                                    v?
5093                                }
5094                                _ => unreachable!(), // We only match specific types above
5095                            };
5096
5097                            // Apply this step's own filter stages (e.g. the
5098                            // `[$substring(title,0,3)='The']` on `.$[...]` in
5099                            // `library.books#$pos.$[...].$pos`) while the tuple
5100                            // bindings are still in scope, so the predicate can
5101                            // reference them and non-matching tuples are dropped.
5102                            let step_result = if step.stages.is_empty() {
5103                                step_result
5104                            } else {
5105                                self.apply_stages(step_result, &step.stages)?
5106                            };
5107
5108                            // Restore previous bindings
5109                            tuple_bindings.restore(self);
5110
5111                            // Rewrap results as tuples carrying this incoming
5112                            // tuple's focus/index/ancestor bindings, so that
5113                            // DOWNSTREAM steps keep seeing them: a predicate like
5114                            // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
5115                            // later `%`/`%.%` in `Account.Order.(Product).{...}`,
5116                            // or a further path step all read those bindings from
5117                            // the tuple wrapper (see AstNode::Variable's tuple
5118                            // fallback). Without rewrapping, the tuple chain is
5119                            // severed after a parenthesized/object/variable step
5120                            // and those references resolve to nothing. The
5121                            // wrappers are projected back to their `@` values by
5122                            // the top-level `unwrap_tuple_output` pass.
5123                            let carried: Vec<(String, JValue)> = tuple_obj
5124                                .iter()
5125                                .filter(|(k, _)| {
5126                                    (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
5127                                })
5128                                .map(|(k, v)| (k.clone(), v.clone()))
5129                                .collect();
5130                            let wrap = |v: JValue| -> JValue {
5131                                match v {
5132                                    // If the step produced a nested tuple stream
5133                                    // (e.g. `(Product)` whose inner `Product` is
5134                                    // itself `%`-tagged), MERGE the inner tuple's
5135                                    // keys over the carried outer bindings, mirroring
5136                                    // jsonata-js's `res.tupleStream` branch
5137                                    // (`Object.assign(tuple, res[bb])`) -- do NOT
5138                                    // double-wrap, which would bury `@`/`!label`
5139                                    // one level down and break a following `%`/`%.%`.
5140                                    JValue::Object(inner)
5141                                        if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
5142                                    {
5143                                        let mut w = IndexMap::new();
5144                                        for (k, val) in &carried {
5145                                            w.insert(k.clone(), val.clone());
5146                                        }
5147                                        for (k, val) in inner.iter() {
5148                                            w.insert(k.clone(), val.clone());
5149                                        }
5150                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
5151                                        JValue::object(w)
5152                                    }
5153                                    other => {
5154                                        let mut w = IndexMap::new();
5155                                        w.insert("@".to_string(), other);
5156                                        for (k, val) in &carried {
5157                                            w.insert(k.clone(), val.clone());
5158                                        }
5159                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
5160                                        JValue::object(w)
5161                                    }
5162                                }
5163                            };
5164                            if !step_result.is_null() && !step_result.is_undefined() {
5165                                // Object constructors yield one value per tuple;
5166                                // other steps may yield an array to splice in.
5167                                if matches!(&step.node, AstNode::Object(_)) {
5168                                    results.push(wrap(step_result));
5169                                } else if let JValue::Array(arr) = step_result {
5170                                    for it in arr.iter() {
5171                                        results.push(wrap(it.clone()));
5172                                    }
5173                                } else {
5174                                    results.push(wrap(step_result));
5175                                }
5176                            }
5177                        }
5178                    }
5179
5180                    current = JValue::array(results);
5181                    continue; // Skip the regular step processing
5182                }
5183            }
5184
5185            current = match &step.node {
5186                AstNode::Wildcard => {
5187                    // Wildcard in path
5188                    let stages = &step.stages;
5189                    let normalized_current = normalize_lazy(&current)?;
5190                    let wildcard_result = match &normalized_current {
5191                        JValue::Object(obj) => {
5192                            let mut result = Vec::new();
5193                            for value in obj.values() {
5194                                // Flatten arrays into the result
5195                                match value {
5196                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
5197                                    _ => result.push(value.clone()),
5198                                }
5199                            }
5200                            JValue::array(result)
5201                        }
5202                        JValue::Array(arr) => {
5203                            // Map wildcard over array
5204                            let mut all_values = Vec::new();
5205                            for item in arr.iter() {
5206                                let normalized_item = normalize_lazy(item)?;
5207                                match &normalized_item {
5208                                    JValue::Object(obj) => {
5209                                        for value in obj.values() {
5210                                            // Flatten arrays
5211                                            match value {
5212                                                JValue::Array(arr) => {
5213                                                    all_values.extend(arr.iter().cloned())
5214                                                }
5215                                                _ => all_values.push(value.clone()),
5216                                            }
5217                                        }
5218                                    }
5219                                    JValue::Array(inner) => {
5220                                        all_values.extend(inner.iter().cloned());
5221                                    }
5222                                    _ => {}
5223                                }
5224                            }
5225                            JValue::array(all_values)
5226                        }
5227                        _ => JValue::Null,
5228                    };
5229
5230                    // Apply stages (predicates) if present
5231                    if !stages.is_empty() {
5232                        self.apply_stages(wildcard_result, stages)?
5233                    } else {
5234                        wildcard_result
5235                    }
5236                }
5237                AstNode::Descendant => {
5238                    // Descendant in path
5239                    match &current {
5240                        JValue::Array(arr) => {
5241                            // Collect descendants from all array elements
5242                            let mut all_descendants = Vec::new();
5243                            for item in arr.iter() {
5244                                all_descendants.extend(self.collect_descendants(item)?);
5245                            }
5246                            JValue::array(all_descendants)
5247                        }
5248                        _ => {
5249                            // Collect descendants from current value
5250                            let descendants = self.collect_descendants(&current)?;
5251                            JValue::array(descendants)
5252                        }
5253                    }
5254                }
5255                AstNode::Name(field_name) => {
5256                    // Navigate into object field or map over array, applying stages
5257                    let stages = &step.stages;
5258
5259                    match &current {
5260                        JValue::Object(obj) => {
5261                            // Single object field extraction - NOT array mapping
5262                            // This resets did_array_mapping because we're extracting from
5263                            // a single value, not mapping over an array. The field's value
5264                            // (even if it's an array) should be preserved as-is.
5265                            did_array_mapping = false;
5266                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
5267                            // Apply stages if present
5268                            if !stages.is_empty() {
5269                                self.apply_stages(val, stages)?
5270                            } else {
5271                                val
5272                            }
5273                        }
5274                        #[cfg(feature = "python")]
5275                        JValue::LazyPyDict(lazy) => {
5276                            did_array_mapping = false;
5277                            let val = lazy.get_field(field_name)?;
5278                            if !stages.is_empty() {
5279                                self.apply_stages(val, stages)?
5280                            } else {
5281                                val
5282                            }
5283                        }
5284                        JValue::Array(arr) => {
5285                            // Array mapping: extract field from each element and apply stages
5286                            did_array_mapping = true; // Track that we did array mapping
5287
5288                            // Fast path: if no elements are tuples and no stages,
5289                            // skip all tuple checking overhead (common case for products.price etc.)
5290                            // Tuples are all-or-nothing (created by index binding #$i),
5291                            // so checking only the first element is sufficient.
5292                            let has_tuples = arr.first().is_some_and(|item| {
5293                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
5294                            });
5295
5296                            if !has_tuples && stages.is_empty() {
5297                                let mut result = Vec::with_capacity(arr.len());
5298                                for item in arr.iter() {
5299                                    match item {
5300                                        JValue::Object(obj) => {
5301                                            if let Some(val) = obj.get(field_name) {
5302                                                if !val.is_null() {
5303                                                    match val {
5304                                                        JValue::Array(arr_val) => {
5305                                                            result.extend(arr_val.iter().cloned())
5306                                                        }
5307                                                        other => result.push(other.clone()),
5308                                                    }
5309                                                }
5310                                            }
5311                                        }
5312                                        JValue::Array(_) => {
5313                                            let nested_result =
5314                                                self.evaluate_path(&[step.clone()], item)?;
5315                                            match nested_result {
5316                                                JValue::Array(nested) => {
5317                                                    result.extend(nested.iter().cloned())
5318                                                }
5319                                                JValue::Null => {}
5320                                                other => result.push(other),
5321                                            }
5322                                        }
5323                                        #[cfg(feature = "python")]
5324                                        JValue::LazyPyDict(lazy) => {
5325                                            let val = lazy.get_field(field_name)?;
5326                                            if !val.is_null() && !val.is_undefined() {
5327                                                match val {
5328                                                    JValue::Array(arr_val) => {
5329                                                        result.extend(arr_val.iter().cloned())
5330                                                    }
5331                                                    other => result.push(other),
5332                                                }
5333                                            }
5334                                        }
5335                                        _ => {}
5336                                    }
5337                                }
5338                                JValue::array(result)
5339                            } else {
5340                                // Full path with tuple support and stages
5341                                let mut result = Vec::new();
5342
5343                                for item in arr.iter() {
5344                                    match item {
5345                                        JValue::Object(obj) => {
5346                                            // Check if this is a tuple stream element
5347                                            let (val, tuple_bindings) = if obj.get("__tuple__")
5348                                                == Some(&JValue::Bool(true))
5349                                            {
5350                                                // This is a tuple - extract '@' value and preserve bindings
5351                                                // Collect index bindings (variables starting with $)
5352                                                let bindings: Vec<(String, JValue)> = obj
5353                                                    .iter()
5354                                                    .filter(|(k, _)| k.starts_with('$'))
5355                                                    .map(|(k, v)| (k.clone(), v.clone()))
5356                                                    .collect();
5357                                                match obj.get("@") {
5358                                                    Some(JValue::Object(inner)) => (
5359                                                        inner
5360                                                            .get(field_name)
5361                                                            .cloned()
5362                                                            .unwrap_or(JValue::Null),
5363                                                        Some(bindings),
5364                                                    ),
5365                                                    #[cfg(feature = "python")]
5366                                                    Some(JValue::LazyPyDict(lazy)) => {
5367                                                        let v = lazy.get_field(field_name)?;
5368                                                        (
5369                                                            if v.is_undefined() {
5370                                                                JValue::Null
5371                                                            } else {
5372                                                                v
5373                                                            },
5374                                                            Some(bindings),
5375                                                        )
5376                                                    }
5377                                                    _ => continue, // Invalid tuple
5378                                                }
5379                                            } else {
5380                                                (
5381                                                    obj.get(field_name)
5382                                                        .cloned()
5383                                                        .unwrap_or(JValue::Null),
5384                                                    None,
5385                                                )
5386                                            };
5387
5388                                            if !val.is_null() {
5389                                                // Helper to wrap value in tuple if we have bindings
5390                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
5391                                                    if let Some(b) = bindings {
5392                                                        let mut wrapper = IndexMap::new();
5393                                                        wrapper.insert("@".to_string(), v);
5394                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5395                                                        for (k, val) in b {
5396                                                            wrapper.insert(k.clone(), val.clone());
5397                                                        }
5398                                                        JValue::object(wrapper)
5399                                                    } else {
5400                                                        v
5401                                                    }
5402                                                };
5403
5404                                                if !stages.is_empty() {
5405                                                    // Bind this tuple's carried focus/index/ancestor
5406                                                    // bindings so a filter predicate that references
5407                                                    // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
5408                                                    // where the `[$l.isbn=isbn]` stage on the (non-focus)
5409                                                    // `books` step must see `$l` from the enclosing
5410                                                    // `@$l` focus stream. Without this the predicate
5411                                                    // evaluates `$l` as unbound and filters everything out.
5412                                                    let saved_tuple: Vec<(String, Option<JValue>)> =
5413                                                        obj.iter()
5414                                                            .filter_map(|(k, _)| {
5415                                                                if let Some(n) = k.strip_prefix('$')
5416                                                                {
5417                                                                    (!n.is_empty())
5418                                                                        .then(|| n.to_string())
5419                                                                } else if k.starts_with('!') {
5420                                                                    Some(k.clone())
5421                                                                } else {
5422                                                                    None
5423                                                                }
5424                                                            })
5425                                                            .map(|n| {
5426                                                                (
5427                                                                    n.clone(),
5428                                                                    self.context
5429                                                                        .lookup(&n)
5430                                                                        .cloned(),
5431                                                                )
5432                                                            })
5433                                                            .collect();
5434                                                    for (k, v) in obj.iter() {
5435                                                        if let Some(n) = k.strip_prefix('$') {
5436                                                            if !n.is_empty() {
5437                                                                self.context
5438                                                                    .bind(n.to_string(), v.clone());
5439                                                            }
5440                                                        } else if k.starts_with('!') {
5441                                                            self.context.bind(k.clone(), v.clone());
5442                                                        }
5443                                                    }
5444                                                    // Apply stages to the extracted value
5445                                                    let processed_val =
5446                                                        self.apply_stages(val, stages);
5447                                                    for (n, old) in saved_tuple.into_iter().rev() {
5448                                                        match old {
5449                                                            Some(v) => self.context.bind(n, v),
5450                                                            None => self.context.unbind(&n),
5451                                                        }
5452                                                    }
5453                                                    let processed_val = processed_val?;
5454                                                    // Stages always return an array (or null); extend results
5455                                                    match processed_val {
5456                                                        JValue::Array(arr) => {
5457                                                            for item in arr.iter() {
5458                                                                result.push(wrap_in_tuple(
5459                                                                    item.clone(),
5460                                                                    &tuple_bindings,
5461                                                                ));
5462                                                            }
5463                                                        }
5464                                                        JValue::Null => {} // Skip nulls from stage application
5465                                                        other => result.push(wrap_in_tuple(
5466                                                            other,
5467                                                            &tuple_bindings,
5468                                                        )),
5469                                                    }
5470                                                } else {
5471                                                    // No stages: flatten arrays, push scalars
5472                                                    // But preserve tuple bindings!
5473                                                    match val {
5474                                                        JValue::Array(arr) => {
5475                                                            for item in arr.iter() {
5476                                                                result.push(wrap_in_tuple(
5477                                                                    item.clone(),
5478                                                                    &tuple_bindings,
5479                                                                ));
5480                                                            }
5481                                                        }
5482                                                        other => result.push(wrap_in_tuple(
5483                                                            other,
5484                                                            &tuple_bindings,
5485                                                        )),
5486                                                    }
5487                                                }
5488                                            }
5489                                        }
5490                                        JValue::Array(_) => {
5491                                            // Recursively map over nested array
5492                                            let nested_result =
5493                                                self.evaluate_path(&[step.clone()], item)?;
5494                                            match nested_result {
5495                                                JValue::Array(nested) => {
5496                                                    result.extend(nested.iter().cloned())
5497                                                }
5498                                                JValue::Null => {}
5499                                                other => result.push(other),
5500                                            }
5501                                        }
5502                                        #[cfg(feature = "python")]
5503                                        JValue::LazyPyDict(lazy) => {
5504                                            // Lazy dicts are never tuples; read directly. Mirrors
5505                                            // the Object arm's non-tuple branch (tuple_bindings =
5506                                            // None throughout, so wrap_in_tuple would be a no-op).
5507                                            let val = lazy.get_field(field_name)?;
5508
5509                                            if !val.is_null() && !val.is_undefined() {
5510                                                if !stages.is_empty() {
5511                                                    let processed_val =
5512                                                        self.apply_stages(val, stages)?;
5513                                                    match processed_val {
5514                                                        JValue::Array(arr) => {
5515                                                            result.extend(arr.iter().cloned())
5516                                                        }
5517                                                        JValue::Null => {} // Skip nulls from stage application
5518                                                        other => result.push(other),
5519                                                    }
5520                                                } else {
5521                                                    match val {
5522                                                        JValue::Array(arr) => {
5523                                                            result.extend(arr.iter().cloned())
5524                                                        }
5525                                                        other => result.push(other),
5526                                                    }
5527                                                }
5528                                            }
5529                                        }
5530                                        _ => {}
5531                                    }
5532                                }
5533
5534                                JValue::array(result)
5535                            }
5536                        }
5537                        JValue::Null => JValue::Null,
5538                        // Accessing field on non-object returns undefined (not an error)
5539                        _ => JValue::Undefined,
5540                    }
5541                }
5542                AstNode::String(string_literal) => {
5543                    // String literal as a path step - evaluate as literal and apply stages
5544                    let stages = &step.stages;
5545                    let val = JValue::string(string_literal.clone());
5546
5547                    if !stages.is_empty() {
5548                        // Apply stages (predicates) to the string literal
5549                        let result = self.apply_stages(val, stages)?;
5550                        // Unwrap single-element arrays back to scalar
5551                        match result {
5552                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
5553                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
5554                            other => other,
5555                        }
5556                    } else {
5557                        val
5558                    }
5559                }
5560                AstNode::Predicate(pred_expr) => {
5561                    // Predicate in path - filter or index into current value
5562                    self.evaluate_predicate(&current, pred_expr)?
5563                }
5564                AstNode::ArrayGroup(elements) => {
5565                    // Array grouping: map expression over array but keep results grouped
5566                    // .[expr] means evaluate expr for each array element
5567                    match &current {
5568                        JValue::Array(arr) => {
5569                            let mut result = Vec::new();
5570                            for item in arr.iter() {
5571                                // For each array item, evaluate all elements and collect results
5572                                let mut group_values = Vec::new();
5573                                for element in elements {
5574                                    let value = self.evaluate_internal(element, item)?;
5575                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
5576                                    // This ensures [[expr]] produces properly nested arrays
5577                                    let should_preserve_array = matches!(
5578                                        element,
5579                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
5580                                    );
5581
5582                                    if should_preserve_array {
5583                                        // Keep the array as a single element to preserve nesting
5584                                        group_values.push(value);
5585                                    } else {
5586                                        // Flatten the value into group_values
5587                                        match value {
5588                                            JValue::Array(arr) => {
5589                                                group_values.extend(arr.iter().cloned())
5590                                            }
5591                                            other => group_values.push(other),
5592                                        }
5593                                    }
5594                                }
5595                                // Each array element gets its own sub-array with all values
5596                                result.push(JValue::array(group_values));
5597                            }
5598                            // jsonata-js's evaluateStep: when this is the path's last
5599                            // step and mapping produced exactly one constructed
5600                            // sub-array, that sub-array IS the path result directly
5601                            // (not wrapped in an outer singleton array) — e.g.
5602                            // `$.[value,epochSeconds]` over a 1-element array yields
5603                            // `[3, 1578381600]`, not `[[3, 1578381600]]`.
5604                            if is_last_step && result.len() == 1 {
5605                                result.into_iter().next().unwrap()
5606                            } else {
5607                                JValue::array(result)
5608                            }
5609                        }
5610                        _ => {
5611                            // For non-arrays, just evaluate the array constructor normally
5612                            let mut result = Vec::new();
5613                            for element in elements {
5614                                let value = self.evaluate_internal(element, &current)?;
5615                                result.push(value);
5616                            }
5617                            JValue::array(result)
5618                        }
5619                    }
5620                }
5621                AstNode::FunctionApplication(expr) => {
5622                    // Function application: map expr over the current value
5623                    // .(expr) means evaluate expr for each element, with $ bound to that element
5624                    // Null/undefined results are filtered out
5625                    //
5626                    // When this parenthesized step is itself tuple-carrying (its
5627                    // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
5628                    // keep the inner path's tuple wrappers so their `!label`
5629                    // bindings survive to the following object/`%` step; the
5630                    // end-of-path projection (or a later consumer) unwraps them.
5631                    let saved_keep = self.keep_tuple_stream;
5632                    if step.is_tuple {
5633                        self.keep_tuple_stream = true;
5634                    }
5635                    let fa_result = match &current {
5636                        JValue::Array(arr) => {
5637                            // Produce the mapped result (compiled fast path or tree-walker fallback).
5638                            // Do NOT return early — singleton unwrapping is applied by the outer
5639                            // path evaluation code after all steps are processed.
5640                            let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
5641                            {
5642                                let shape = arr.first().and_then(build_shape_cache);
5643                                let mut result = Vec::with_capacity(arr.len());
5644                                for item in arr.iter() {
5645                                    let value = if let Some(ref s) = shape {
5646                                        eval_compiled_shaped(
5647                                            &compiled,
5648                                            item,
5649                                            None,
5650                                            s,
5651                                            &self.options,
5652                                            self.start_time,
5653                                        )?
5654                                    } else {
5655                                        eval_compiled(
5656                                            &compiled,
5657                                            item,
5658                                            None,
5659                                            &self.options,
5660                                            self.start_time,
5661                                        )?
5662                                    };
5663                                    if !value.is_null() && !value.is_undefined() {
5664                                        result.push(value);
5665                                    }
5666                                }
5667                                result
5668                            } else {
5669                                let mut result = Vec::new();
5670                                for item in arr.iter() {
5671                                    // Save the current $ binding
5672                                    let saved_dollar = self.context.lookup("$").cloned();
5673
5674                                    // Bind $ to the current item
5675                                    self.context.bind("$".to_string(), item.clone());
5676
5677                                    // Evaluate the expression in the context of this item
5678                                    let value = self.evaluate_internal(expr, item)?;
5679
5680                                    // Restore the previous $ binding
5681                                    if let Some(saved) = saved_dollar {
5682                                        self.context.bind("$".to_string(), saved);
5683                                    } else {
5684                                        self.context.unbind("$");
5685                                    }
5686
5687                                    // Only include non-null/undefined values
5688                                    if !value.is_null() && !value.is_undefined() {
5689                                        result.push(value);
5690                                    }
5691                                }
5692                                result
5693                            };
5694                            // Don't do singleton unwrapping here - let the path result
5695                            // handling deal with it, which respects has_explicit_array_keep
5696                            JValue::array(mapped)
5697                        }
5698                        _ => {
5699                            // For non-arrays, bind $ and evaluate
5700                            let saved_dollar = self.context.lookup("$").cloned();
5701                            self.context.bind("$".to_string(), current.clone());
5702
5703                            let value = self.evaluate_internal(expr, &current)?;
5704
5705                            if let Some(saved) = saved_dollar {
5706                                self.context.bind("$".to_string(), saved);
5707                            } else {
5708                                self.context.unbind("$");
5709                            }
5710
5711                            value
5712                        }
5713                    };
5714                    self.keep_tuple_stream = saved_keep;
5715                    fa_result
5716                }
5717                AstNode::Sort { terms, .. } => {
5718                    // Sort as a path step - sort 'current' by the terms
5719                    self.evaluate_sort(&current, terms)?
5720                }
5721                // Handle complex path steps (e.g., computed properties, object construction)
5722                _ => {
5723                    let saved_keep = self.keep_tuple_stream;
5724                    if step.is_tuple {
5725                        self.keep_tuple_stream = true;
5726                    }
5727                    let v = self.evaluate_path_step(&step.node, &current, data);
5728                    self.keep_tuple_stream = saved_keep;
5729                    v?
5730                }
5731            };
5732        }
5733
5734        // End-of-path tuple projection, mirroring jsonata-js evaluatePath
5735        // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
5736        // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
5737        // wrappers are internal bookkeeping and must not escape into an enclosing
5738        // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
5739        // `=` compare wrapper objects and always yield false). Suppressed only for
5740        // the two consumers that read the carried bindings directly off the
5741        // wrappers (Sort input, ObjectTransform/group-by input), which set
5742        // `keep_tuple_stream`. The top-level `evaluate()` still runs
5743        // `unwrap_tuple_output` as a backstop for wrappers nested inside
5744        // constructed output.
5745        if !self.keep_tuple_stream {
5746            if let JValue::Array(arr) = &current {
5747                let is_tuple_stream = arr.first().is_some_and(|f| {
5748                    matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5749                });
5750                if is_tuple_stream {
5751                    let projected: Vec<JValue> = arr
5752                        .iter()
5753                        .map(|t| match t {
5754                            JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
5755                            other => other.clone(),
5756                        })
5757                        .collect();
5758                    current = JValue::array(projected);
5759                }
5760            }
5761        }
5762
5763        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5764        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5765
5766        // Check for explicit array-keeping operations. Empty predicate `[]` can
5767        // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5768        // stage; it also counts when it sits inside a `Sort` step's input path
5769        // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5770        // the sort and the trailing index so the singleton stays `[4]`.
5771        let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5772
5773        // Unwrap when:
5774        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5775        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5776        //    Note: did_array_mapping is reset to false when extracting from a single object,
5777        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5778        // BUT NOT when there's an explicit array-keeping operation
5779        //
5780        // Important: We DON'T unwrap just because original data was an array - what matters is
5781        // whether the final extraction was from an array mapping context or a single object.
5782        let should_unwrap = !has_explicit_array_keep
5783            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5784
5785        let result = match &current {
5786            // An empty result sequence is "no value" -> undefined (jsonata-js
5787            // treats an empty sequence, e.g. from a filter that matched nothing,
5788            // as undefined so a following `.field` and object/array construction
5789            // drop it rather than keeping an explicit null). `[]` array-keep is
5790            // handled separately above via has_explicit_array_keep.
5791            JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5792            // Unwrap singleton arrays when appropriate
5793            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5794            // Keep arrays otherwise
5795            _ => current,
5796        };
5797
5798        // An explicit `[]` keep-array forces the result to remain an array even
5799        // after a later singleton index collapses it to a scalar (jsonata's
5800        // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5801        let result = if has_explicit_array_keep
5802            && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5803        {
5804            JValue::array(vec![result])
5805        } else {
5806            result
5807        };
5808
5809        if let JValue::Array(arr) = &result {
5810            check_sequence_length(arr.len(), &self.options)?;
5811        }
5812
5813        Ok(result)
5814    }
5815
5816    /// True when a path step carries a tuple-binding flag (`@$var` focus,
5817    /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5818    /// produce/extend a tuple stream rather than be evaluated as a plain step.
5819    ///
5820    fn step_creates_tuple(step: &PathStep) -> bool {
5821        step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5822    }
5823
5824    /// True when a path contains an explicit empty predicate `[]` (keep-array),
5825    /// either directly as a step/stage or nested inside a `Sort` step's input
5826    /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5827    /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5828    /// -> `[4]`).
5829    fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5830        steps.iter().any(|step| {
5831            if let AstNode::Predicate(pred) = &step.node {
5832                if matches!(**pred, AstNode::Boolean(true)) {
5833                    return true;
5834                }
5835            }
5836            if step.stages.iter().any(
5837                |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5838            ) {
5839                return true;
5840            }
5841            if let AstNode::Sort { input, .. } = &step.node {
5842                if let AstNode::Path { steps: inner } = input.as_ref() {
5843                    return Self::path_keeps_singleton_array(inner);
5844                }
5845            }
5846            false
5847        })
5848    }
5849
5850    /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5851    /// scope, saving whatever was previously bound under each of those names
5852    /// so [`TupleKeyBindings::restore`] can put it back afterward.
5853    ///
5854    /// This is the single shared implementation of the
5855    /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5856    /// pattern that recurs across `create_tuple_stream`,
5857    /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5858    /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5859    /// none of those call sites can regress to a blind `unbind` (which
5860    /// deletes rather than restores a same-named outer `:=` binding that was
5861    /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5862    /// binding silently clobbering an outer variable of the same name).
5863    fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5864        let mut saved = Vec::new();
5865        for (key, value) in tuple_obj.iter() {
5866            let name = if let Some(n) = key.strip_prefix('$') {
5867                if n.is_empty() {
5868                    continue;
5869                }
5870                n.to_string()
5871            } else if key.starts_with('!') {
5872                key.clone()
5873            } else {
5874                continue;
5875            };
5876            saved.push((name.clone(), self.context.lookup(&name).cloned()));
5877            self.context.bind(name, value.clone());
5878        }
5879        TupleKeyBindings { saved }
5880    }
5881
5882    /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5883    /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5884    /// vector holds `JValue::Object` tuple wrappers of the shape
5885    /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5886    /// which downstream steps consume via the existing tuple-aware handling in
5887    /// `evaluate_path`.
5888    ///
5889    /// `input` is the previous step's result: either an already-built tuple
5890    /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5891    /// plain value/array entering tuple mode for the first time (each item
5892    /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5893    ///
5894    /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5895    /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5896    /// fast paths only *rebuild* a wrapper around a value pulled from an input
5897    /// element that is already `__tuple__`-tagged, which can only be true if a
5898    /// `create_tuple_stream` call already ran earlier in this evaluation and set
5899    /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5900    /// fire on a value that did NOT come from an existing tuple stream, it must
5901    /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5902    /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5903    fn create_tuple_stream(
5904        &mut self,
5905        step: &PathStep,
5906        input: &JValue,
5907        is_first_path_step: bool,
5908    ) -> Result<Vec<JValue>, EvaluatorError> {
5909        use std::rc::Rc;
5910
5911        // Mark that this evaluate() call produced tuple wrappers, so the
5912        // top-level `evaluate()` knows to run the output-unwrap pass.
5913        self.tuple_stream_created = true;
5914
5915        // Gather the incoming tuple bindings.
5916        let is_tuple_input = matches!(
5917            input,
5918            JValue::Array(arr) if arr.first().is_some_and(|f| {
5919                matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5920            })
5921        );
5922        let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5923            match input {
5924                JValue::Array(arr) => arr
5925                    .iter()
5926                    .filter_map(|t| match t {
5927                        JValue::Object(o) => Some(o.clone()),
5928                        _ => None,
5929                    })
5930                    .collect(),
5931                _ => unreachable!(),
5932            }
5933        } else {
5934            let items: Vec<JValue> = match input {
5935                // Mirrors jsonata-js evaluatePath's inputSequence rule
5936                // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5937                // when the path's FIRST step is a variable reference (`$`/`$$`) the
5938                // input array is taken as a SINGLE sequence value
5939                // (`createSequence(input)`) rather than iterated per-element. We
5940                // only need this for a leading INDEX bind (`$#$pos`): the whole
5941                // array becomes one incoming tuple whose `@` is the array, then
5942                // the inner position counter walks its elements so `$pos` runs
5943                // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5944                // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5945                // the step input, so a single binding would yield one copy of the
5946                // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5947                // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5948                // `$.$#$pos` (a later step) still iterates per-element.
5949                JValue::Array(arr)
5950                    if !(is_first_path_step
5951                        && matches!(&step.node, AstNode::Variable(_))
5952                        && step.index_var.is_some()) =>
5953                {
5954                    arr.iter().cloned().collect()
5955                }
5956                single => vec![single.clone()],
5957            };
5958            items
5959                .into_iter()
5960                .map(|item| {
5961                    let mut wrapper = IndexMap::new();
5962                    wrapper.insert("@".to_string(), item);
5963                    wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5964                    Rc::new(wrapper)
5965                })
5966                .collect()
5967        };
5968
5969        // A sort step in a tuple stream orders the WHOLE stream (not per element)
5970        // and re-tuples with the index = sorted position, mirroring jsonata-js
5971        // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5972        // array, then number the sorted values, then filter by `$pos`.
5973        if let AstNode::Sort { terms, .. } = &step.node {
5974            let stream = JValue::array(
5975                incoming
5976                    .iter()
5977                    .map(|t| JValue::object((**t).clone()))
5978                    .collect(),
5979            );
5980            // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5981            // carried keys bound), returning the wrappers in sorted order.
5982            let sorted = self.evaluate_sort(&stream, terms)?;
5983            let sorted_arr: Vec<JValue> = match sorted {
5984                JValue::Array(a) => a.iter().cloned().collect(),
5985                JValue::Null | JValue::Undefined => Vec::new(),
5986                other => vec![other],
5987            };
5988            let mut result = Vec::new();
5989            for (ss, elem) in sorted_arr.into_iter().enumerate() {
5990                let mut new_tuple = match elem {
5991                    JValue::Object(o) => (*o).clone(),
5992                    other => {
5993                        let mut m = IndexMap::new();
5994                        m.insert("@".to_string(), other);
5995                        m
5996                    }
5997                };
5998                if let Some(index_var) = &step.index_var {
5999                    new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
6000                }
6001                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
6002                result.push(JValue::object(new_tuple));
6003            }
6004            return Ok(result);
6005        }
6006
6007        let mut result = Vec::new();
6008        for tuple_obj in incoming {
6009            // Bind every carried tuple key into a real scope frame so the step
6010            // expression can see prior focus/index/ancestor bindings, mirroring
6011            // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
6012            // Saves/restores rather than blindly unbinding, so a tuple key
6013            // whose name collides with a live outer `:=` binding doesn't get
6014            // deleted once this tuple row's evaluation is done.
6015            let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
6016
6017            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
6018            let step_value = self.evaluate_internal(&step.node, &actual_data);
6019
6020            let mut step_value = step_value?;
6021            // When the step carries an ORDERED index stage (a second `#$var`,
6022            // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
6023            // BUILT tuple stream in order (filter then re-number) so the filter
6024            // sees the per-tuple focus/index bindings and each index reflects the
6025            // position at its point in the sequence. Those steps defer all stage
6026            // application to `apply_tuple_stages` after the stream is built.
6027            let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
6028            if !step.stages.is_empty() && !has_index_stage {
6029                // A `%` inside a filter predicate refers to the ancestry of
6030                // THIS step (its own input for a level-1 `%`, or an earlier
6031                // step's input for a `%.%` chain). ast_transform tags this step
6032                // with `ancestor_label`; bind it to the step's input so the
6033                // level-1 `%` resolves. The `%.%` chain's deeper references use
6034                // labels carried in the INCOMING tuple, so those bindings
6035                // (`tuple_bindings`) must stay live through `apply_stages` --
6036                // their restore is deferred until after it (previously they
6037                // were unbound first, which silently broke `%.%` inside
6038                // predicates).
6039                let own_label = match &step.ancestor_label {
6040                    Some(label) if !tuple_bindings.contains(label) => {
6041                        self.context.bind(label.clone(), actual_data.clone());
6042                        Some(label.clone())
6043                    }
6044                    _ => None,
6045                };
6046                step_value = self.apply_stages(step_value, &step.stages)?;
6047                if let Some(label) = own_label {
6048                    self.context.unbind(&label);
6049                }
6050            }
6051
6052            tuple_bindings.restore(self);
6053
6054            let row: Vec<JValue> = match step_value {
6055                JValue::Undefined => continue,
6056                JValue::Array(arr) => arr.iter().cloned().collect(),
6057                other => vec![other],
6058            };
6059
6060            for (position, value) in row.into_iter().enumerate() {
6061                if value.is_undefined() {
6062                    continue;
6063                }
6064                let mut new_tuple = (*tuple_obj).clone();
6065                if let Some(focus_var) = &step.focus {
6066                    // Focus binding keeps `@` as this step's INPUT (already carried
6067                    // in the cloned tuple) and binds the result to `$focus`,
6068                    // matching jsonata-js: `tuple[expr.focus] = res[bb];
6069                    // tuple['@'] = tupleBindings[ee]['@'];`.
6070                    new_tuple.insert(format!("${}", focus_var), value);
6071                } else {
6072                    new_tuple.insert("@".to_string(), value);
6073                }
6074                if let Some(index_var) = &step.index_var {
6075                    // Index binding records the position of this value WITHIN the
6076                    // per-binding result row (jsonata-js evaluateTupleStep: the
6077                    // inner `bb` counter, `tuple[expr.index] = bb`), which resets
6078                    // for each incoming tuple.
6079                    new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
6080                }
6081                if let Some(ancestor_label) = &step.ancestor_label {
6082                    // `%` ancestor: preserve this step's INPUT under the label.
6083                    new_tuple.insert(ancestor_label.clone(), actual_data.clone());
6084                }
6085                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
6086                result.push(JValue::object(new_tuple));
6087            }
6088        }
6089
6090        // Apply ordered filter/index stages to the built tuple stream when a
6091        // second index binding deferred them (see the has_index_stage comment
6092        // in the build loop above).
6093        if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
6094            result = self.apply_tuple_stages(result, &step.stages)?;
6095        }
6096
6097        Ok(result)
6098    }
6099
6100    /// Apply a step's stages, in order, to an already-built tuple stream --
6101    /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
6102    /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
6103    /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
6104    /// stage sets its variable on every surviving tuple to that tuple's position
6105    /// in the CURRENT stream. Used for steps carrying a second `#$var` index
6106    /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
6107    /// pre-filter position and `$ib2` the post-filter position.
6108    fn apply_tuple_stages(
6109        &mut self,
6110        mut tuples: Vec<JValue>,
6111        stages: &[Stage],
6112    ) -> Result<Vec<JValue>, EvaluatorError> {
6113        for stage in stages {
6114            match stage {
6115                Stage::Filter(pred) => {
6116                    let mut kept = Vec::with_capacity(tuples.len());
6117                    for tup in tuples.into_iter() {
6118                        let JValue::Object(obj) = &tup else {
6119                            continue;
6120                        };
6121                        // Bind this tuple's carried focus/index/ancestor keys so
6122                        // the predicate can reference them (save/restore rather
6123                        // than blind unbind -- see bind_tuple_keys).
6124                        let tuple_bindings = self.bind_tuple_keys(obj);
6125                        let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
6126                        let pred_res = self.evaluate_internal(pred, &at);
6127                        tuple_bindings.restore(self);
6128                        if self.is_truthy(&pred_res?) {
6129                            kept.push(tup);
6130                        }
6131                    }
6132                    tuples = kept;
6133                }
6134                Stage::Index(var) => {
6135                    for (pos, tup) in tuples.iter_mut().enumerate() {
6136                        if let JValue::Object(obj) = tup {
6137                            let mut m = (**obj).clone();
6138                            m.insert(format!("${}", var), JValue::from(pos as i64));
6139                            *tup = JValue::object(m);
6140                        }
6141                    }
6142                }
6143            }
6144        }
6145        Ok(tuples)
6146    }
6147
6148    /// Helper to evaluate a complex path step
6149    fn evaluate_path_step(
6150        &mut self,
6151        step: &AstNode,
6152        current: &JValue,
6153        original_data: &JValue,
6154    ) -> Result<JValue, EvaluatorError> {
6155        // Special case: array mapping with object construction
6156        // e.g., items.{"name": name, "price": price}
6157        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
6158            match (current, step) {
6159                (JValue::Array(arr), AstNode::Object(pairs)) => {
6160                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
6161                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
6162                        let shape = arr.first().and_then(build_shape_cache);
6163                        let mut mapped = Vec::with_capacity(arr.len());
6164                        for item in arr.iter() {
6165                            let result = if let Some(ref s) = shape {
6166                                eval_compiled_shaped(
6167                                    &compiled,
6168                                    item,
6169                                    None,
6170                                    s,
6171                                    &self.options,
6172                                    self.start_time,
6173                                )?
6174                            } else {
6175                                eval_compiled(
6176                                    &compiled,
6177                                    item,
6178                                    None,
6179                                    &self.options,
6180                                    self.start_time,
6181                                )?
6182                            };
6183                            if !result.is_undefined() {
6184                                mapped.push(result);
6185                            }
6186                        }
6187                        return Ok(JValue::array(mapped));
6188                    }
6189                    // Fallback: full AST evaluation per element
6190                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
6191                        .iter()
6192                        .map(|item| self.evaluate_internal(step, item))
6193                        .collect();
6194                    Ok(JValue::array(mapped?))
6195                }
6196                _ => unreachable!(),
6197            }
6198        } else {
6199            // Special case: array.$ should map $ over the array, returning each element
6200            // e.g., [1, 2, 3].$ returns [1, 2, 3]
6201            if let AstNode::Variable(name) = step {
6202                if name.is_empty() {
6203                    // Bare $ - map over array if current is an array
6204                    if let JValue::Array(arr) = current {
6205                        // Map $ over each element - $ refers to each element in turn
6206                        return Ok(JValue::Array(arr.clone()));
6207                    } else {
6208                        // For non-arrays, $ refers to the current value
6209                        return Ok(current.clone());
6210                    }
6211                }
6212            }
6213
6214            // Special case: Variable access on tuple arrays (from index binding #$var)
6215            // When current is a tuple array, we need to evaluate the variable against each tuple
6216            // so that tuple bindings ($i, etc.) can be found
6217            if matches!(step, AstNode::Variable(_)) {
6218                if let JValue::Array(arr) = current {
6219                    // Check if this is a tuple array
6220                    let is_tuple_array = arr.first().is_some_and(|first| {
6221                        if let JValue::Object(obj) = first {
6222                            obj.get("__tuple__") == Some(&JValue::Bool(true))
6223                        } else {
6224                            false
6225                        }
6226                    });
6227
6228                    if is_tuple_array {
6229                        // Map the variable lookup over each tuple
6230                        let mut results = Vec::new();
6231                        for tuple in arr.iter() {
6232                            // Evaluate the variable in the context of this tuple
6233                            // This allows tuple bindings ($i, etc.) to be found
6234                            let val = self.evaluate_internal(step, tuple)?;
6235                            if !val.is_null() && !val.is_undefined() {
6236                                results.push(val);
6237                            }
6238                        }
6239                        return Ok(JValue::array(results));
6240                    }
6241                }
6242            }
6243
6244            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
6245            // rather than being used to index/access the current value
6246            // e.g., items[price > 50] where [price > 50] is a filter operation
6247            // or $x.price where $x is a variable binding
6248            // or $$.field where $$ is the parent context
6249            // or [0..9] where it's an array constructor
6250            // or $^(field) where it's a sort operator
6251            // or (expr).field where (expr) is a block that evaluates to a value
6252            if matches!(
6253                step,
6254                AstNode::Binary { .. }
6255                    | AstNode::Function { .. }
6256                    | AstNode::Variable(_)
6257                    | AstNode::ParentVariable(_)
6258                    | AstNode::Parent(_)
6259                    | AstNode::Array(_)
6260                    | AstNode::Object(_)
6261                    | AstNode::Sort { .. }
6262                    | AstNode::Block(_)
6263            ) {
6264                // Evaluate the step in the context of original_data and return the result directly
6265                return self.evaluate_internal(step, original_data);
6266            }
6267
6268            // Standard path step evaluation for indexing/accessing current value
6269            let step_value = self.evaluate_internal(step, original_data)?;
6270            Ok(match (current, &step_value) {
6271                (JValue::Object(obj), JValue::String(key)) => {
6272                    obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
6273                }
6274                #[cfg(feature = "python")]
6275                (JValue::LazyPyDict(lazy), JValue::String(key)) => lazy.get_field(key)?,
6276                (JValue::Array(arr), JValue::Number(n)) => {
6277                    let index = *n as i64;
6278                    let len = arr.len() as i64;
6279
6280                    // Handle negative indexing (offset from end)
6281                    let actual_idx = if index < 0 { len + index } else { index };
6282
6283                    if actual_idx < 0 || actual_idx >= len {
6284                        JValue::Undefined
6285                    } else {
6286                        arr[actual_idx as usize].clone()
6287                    }
6288                }
6289                _ => JValue::Undefined,
6290            })
6291        }
6292    }
6293
6294    /// Evaluate a binary operation
6295    fn evaluate_binary_op(
6296        &mut self,
6297        op: crate::ast::BinaryOp,
6298        lhs: &AstNode,
6299        rhs: &AstNode,
6300        data: &JValue,
6301    ) -> Result<JValue, EvaluatorError> {
6302        use crate::ast::BinaryOp;
6303
6304        // Special handling for coalescing operator (??)
6305        // Returns right side if left is undefined (produces no value)
6306        // Note: literal null is a value, so it's NOT replaced
6307        if op == BinaryOp::Coalesce {
6308            // Try to evaluate the left side
6309            return match self.evaluate_internal(lhs, data) {
6310                Ok(value) => {
6311                    // Successfully evaluated to a value (even if it's null)
6312                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
6313                    if matches!(lhs, AstNode::Null) {
6314                        Ok(value)
6315                    }
6316                    // For paths and variables, undefined (no match/unbound) - use RHS
6317                    else if value.is_undefined()
6318                        && (matches!(lhs, AstNode::Path { .. })
6319                            || matches!(lhs, AstNode::String(_))
6320                            || matches!(lhs, AstNode::Variable(_)))
6321                    {
6322                        self.evaluate_internal(rhs, data)
6323                    } else {
6324                        Ok(value)
6325                    }
6326                }
6327                Err(_) => {
6328                    // Evaluation failed (e.g., undefined variable) - use RHS
6329                    self.evaluate_internal(rhs, data)
6330                }
6331            };
6332        }
6333
6334        // Special handling for default operator (?:)
6335        // Returns right side if left is falsy or a non-value (like a function)
6336        if op == BinaryOp::Default {
6337            let left = self.evaluate_internal(lhs, data)?;
6338            if self.is_truthy_for_default(&left) {
6339                return Ok(left);
6340            }
6341            return self.evaluate_internal(rhs, data);
6342        }
6343
6344        // Special handling for chain/pipe operator (~>)
6345        // Pipes the LHS result to the RHS function as the first argument
6346        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
6347        if op == BinaryOp::ChainPipe {
6348            // Handle regex on RHS - treat as $match(lhs, regex)
6349            if let AstNode::Regex { pattern, flags } = rhs {
6350                // Evaluate LHS
6351                let lhs_value = self.evaluate_internal(lhs, data)?;
6352                // Do regex match inline
6353                return match lhs_value {
6354                    JValue::String(s) => {
6355                        // Build the regex
6356                        let case_insensitive = flags.contains('i');
6357                        let regex_pattern = if case_insensitive {
6358                            format!("(?i){}", pattern)
6359                        } else {
6360                            pattern.clone()
6361                        };
6362                        match regex::Regex::new(&regex_pattern) {
6363                            Ok(re) => {
6364                                if let Some(m) = re.find(&s) {
6365                                    // Return match object
6366                                    let mut result = IndexMap::new();
6367                                    result.insert(
6368                                        "match".to_string(),
6369                                        JValue::string(m.as_str().to_string()),
6370                                    );
6371                                    result.insert(
6372                                        "start".to_string(),
6373                                        JValue::Number(m.start() as f64),
6374                                    );
6375                                    result
6376                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
6377
6378                                    // Capture groups
6379                                    let mut groups = Vec::new();
6380                                    for cap in re.captures_iter(&s).take(1) {
6381                                        for i in 1..cap.len() {
6382                                            if let Some(c) = cap.get(i) {
6383                                                groups.push(JValue::string(c.as_str().to_string()));
6384                                            }
6385                                        }
6386                                    }
6387                                    if !groups.is_empty() {
6388                                        result.insert("groups".to_string(), JValue::array(groups));
6389                                    }
6390
6391                                    Ok(JValue::object(result))
6392                                } else {
6393                                    Ok(JValue::Null)
6394                                }
6395                            }
6396                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
6397                                "Invalid regex: {}",
6398                                e
6399                            ))),
6400                        }
6401                    }
6402                    JValue::Null => Ok(JValue::Null),
6403                    _ => Err(EvaluatorError::TypeError(
6404                        "Left side of ~> /regex/ must be a string".to_string(),
6405                    )),
6406                };
6407            }
6408
6409            // Early check: if LHS evaluates to undefined, return undefined
6410            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
6411            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
6412            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
6413                return Ok(JValue::Undefined);
6414            }
6415
6416            // Handle different RHS types
6417            match rhs {
6418                AstNode::Function {
6419                    name,
6420                    args,
6421                    is_builtin,
6422                } => {
6423                    // RHS is a function call
6424                    // Check if the function call has placeholder arguments (partial application)
6425                    let has_placeholder =
6426                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6427
6428                    if has_placeholder {
6429                        // Partial application: replace the first placeholder with LHS value
6430                        let lhs_value = self.evaluate_internal(lhs, data)?;
6431                        let mut filled_args = Vec::new();
6432                        let mut lhs_used = false;
6433
6434                        for arg in args.iter() {
6435                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
6436                                // Replace first placeholder with evaluated LHS
6437                                // We need to create a temporary binding to pass the value
6438                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
6439                                self.context.bind(temp_name.clone(), lhs_value.clone());
6440                                filled_args.push(AstNode::Variable(temp_name));
6441                                lhs_used = true;
6442                            } else {
6443                                filled_args.push(arg.clone());
6444                            }
6445                        }
6446
6447                        // Evaluate the function with filled args
6448                        let result =
6449                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
6450
6451                        // Clean up temp bindings
6452                        for (i, arg) in args.iter().enumerate() {
6453                            if matches!(arg, AstNode::Placeholder) {
6454                                self.context.unbind(&format!("__pipe_arg_{}", i));
6455                            }
6456                        }
6457
6458                        // Unwrap singleton results from chain operator
6459                        return result.map(|v| self.unwrap_singleton(v));
6460                    } else {
6461                        // No placeholders: build args list with LHS as first argument
6462                        let mut all_args = vec![lhs.clone()];
6463                        all_args.extend_from_slice(args);
6464                        // Unwrap singleton results from chain operator
6465                        return self
6466                            .evaluate_function_call(name, &all_args, *is_builtin, data)
6467                            .map(|v| self.unwrap_singleton(v));
6468                    }
6469                }
6470                AstNode::Variable(var_name) => {
6471                    // RHS is a function reference (no parens)
6472                    // e.g., $average($tempReadings) ~> $round
6473                    let all_args = vec![lhs.clone()];
6474                    // Unwrap singleton results from chain operator
6475                    return self
6476                        .evaluate_function_call(var_name, &all_args, true, data)
6477                        .map(|v| self.unwrap_singleton(v));
6478                }
6479                AstNode::Binary {
6480                    op: BinaryOp::ChainPipe,
6481                    ..
6482                } => {
6483                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
6484                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
6485                    let lhs_value = self.evaluate_internal(lhs, data)?;
6486                    return self.evaluate_internal(rhs, &lhs_value);
6487                }
6488                AstNode::Transform { .. } => {
6489                    // RHS is a transform - invoke it with LHS as input
6490                    // Evaluate LHS first
6491                    let lhs_value = self.evaluate_internal(lhs, data)?;
6492
6493                    // Bind $ to the LHS value, then evaluate the transform
6494                    let saved_binding = self.context.lookup("$").cloned();
6495                    self.context.bind("$".to_string(), lhs_value.clone());
6496
6497                    let result = self.evaluate_internal(rhs, data);
6498
6499                    // Restore $ binding
6500                    if let Some(saved) = saved_binding {
6501                        self.context.bind("$".to_string(), saved);
6502                    } else {
6503                        self.context.unbind("$");
6504                    }
6505
6506                    // Unwrap singleton results from chain operator
6507                    return result.map(|v| self.unwrap_singleton(v));
6508                }
6509                AstNode::Lambda {
6510                    params,
6511                    body,
6512                    signature,
6513                    thunk,
6514                } => {
6515                    // RHS is a lambda - invoke it with LHS as argument
6516                    let lhs_value = self.evaluate_internal(lhs, data)?;
6517                    // Unwrap singleton results from chain operator
6518                    return self
6519                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
6520                        .map(|v| self.unwrap_singleton(v));
6521                }
6522                AstNode::Path { steps } => {
6523                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
6524                    // If the first step is a function call, we need to add LHS as first argument
6525                    if let Some(first_step) = steps.first() {
6526                        match &first_step.node {
6527                            AstNode::Function {
6528                                name,
6529                                args,
6530                                is_builtin,
6531                            } => {
6532                                // Prepend LHS to the function arguments
6533                                let mut all_args = vec![lhs.clone()];
6534                                all_args.extend_from_slice(args);
6535
6536                                // Call the function
6537                                let mut result = self.evaluate_function_call(
6538                                    name,
6539                                    &all_args,
6540                                    *is_builtin,
6541                                    data,
6542                                )?;
6543
6544                                // Apply stages from the first step (e.g., predicates)
6545                                for stage in &first_step.stages {
6546                                    match stage {
6547                                        Stage::Filter(filter_expr) => {
6548                                            result = self.evaluate_predicate_as_stage(
6549                                                &result,
6550                                                filter_expr,
6551                                            )?;
6552                                        }
6553                                        Stage::Index(_) => {}
6554                                    }
6555                                }
6556
6557                                // Apply remaining path steps if any
6558                                if steps.len() > 1 {
6559                                    let remaining_path = AstNode::Path {
6560                                        steps: steps[1..].to_vec(),
6561                                    };
6562                                    result = self.evaluate_internal(&remaining_path, &result)?;
6563                                }
6564
6565                                // Unwrap singleton results from chain operator, unless there are stages
6566                                // Stages (like predicates) indicate we want to preserve array structure
6567                                if !first_step.stages.is_empty() || steps.len() > 1 {
6568                                    return Ok(result);
6569                                } else {
6570                                    return Ok(self.unwrap_singleton(result));
6571                                }
6572                            }
6573                            AstNode::Variable(var_name) => {
6574                                // Variable that should resolve to a function
6575                                let all_args = vec![lhs.clone()];
6576                                let mut result =
6577                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
6578
6579                                // Apply stages from the first step
6580                                for stage in &first_step.stages {
6581                                    match stage {
6582                                        Stage::Filter(filter_expr) => {
6583                                            result = self.evaluate_predicate_as_stage(
6584                                                &result,
6585                                                filter_expr,
6586                                            )?;
6587                                        }
6588                                        Stage::Index(_) => {}
6589                                    }
6590                                }
6591
6592                                // Apply remaining path steps if any
6593                                if steps.len() > 1 {
6594                                    let remaining_path = AstNode::Path {
6595                                        steps: steps[1..].to_vec(),
6596                                    };
6597                                    result = self.evaluate_internal(&remaining_path, &result)?;
6598                                }
6599
6600                                // Unwrap singleton results from chain operator, unless there are stages
6601                                // Stages (like predicates) indicate we want to preserve array structure
6602                                if !first_step.stages.is_empty() || steps.len() > 1 {
6603                                    return Ok(result);
6604                                } else {
6605                                    return Ok(self.unwrap_singleton(result));
6606                                }
6607                            }
6608                            _ => {
6609                                // Other path types - just evaluate normally with LHS as context
6610                                let lhs_value = self.evaluate_internal(lhs, data)?;
6611                                return self
6612                                    .evaluate_internal(rhs, &lhs_value)
6613                                    .map(|v| self.unwrap_singleton(v));
6614                            }
6615                        }
6616                    }
6617
6618                    // Empty path? Shouldn't happen, but handle it
6619                    let lhs_value = self.evaluate_internal(lhs, data)?;
6620                    return self
6621                        .evaluate_internal(rhs, &lhs_value)
6622                        .map(|v| self.unwrap_singleton(v));
6623                }
6624                _ => {
6625                    return Err(EvaluatorError::TypeError(
6626                        "Right side of ~> must be a function call or function reference"
6627                            .to_string(),
6628                    ));
6629                }
6630            }
6631        }
6632
6633        // Special handling for variable binding (:=)
6634        if op == BinaryOp::ColonEqual {
6635            // Extract variable name from LHS
6636            let var_name = match lhs {
6637                AstNode::Variable(name) => name.clone(),
6638                _ => {
6639                    return Err(EvaluatorError::TypeError(
6640                        "Left side of := must be a variable".to_string(),
6641                    ))
6642                }
6643            };
6644
6645            // Check if RHS is a lambda - store it specially
6646            if let AstNode::Lambda {
6647                params,
6648                body,
6649                signature,
6650                thunk,
6651            } = rhs
6652            {
6653                // Store the lambda AST for later invocation
6654                // Capture only the free variables referenced by the lambda body
6655                let captured_env = self.capture_environment_for(body, params);
6656                let compiled_body = if !thunk {
6657                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
6658                    try_compile_expr_with_allowed_vars(body, &var_refs)
6659                } else {
6660                    None
6661                };
6662                let stored_lambda = StoredLambda {
6663                    params: params.clone(),
6664                    body: (**body).clone(),
6665                    compiled_body,
6666                    signature: signature.clone(),
6667                    captured_env,
6668                    captured_data: Some(data.clone()),
6669                    thunk: *thunk,
6670                };
6671                let lambda_params = stored_lambda.params.clone();
6672                let lambda_sig = stored_lambda.signature.clone();
6673                self.context.bind_lambda(var_name.clone(), stored_lambda);
6674
6675                // Return a lambda marker value (include _lambda_id so it can be found later)
6676                let lambda_repr = JValue::lambda(
6677                    var_name.as_str(),
6678                    lambda_params,
6679                    Some(var_name.clone()),
6680                    lambda_sig,
6681                );
6682                return Ok(lambda_repr);
6683            }
6684
6685            // Check if RHS is a pure function composition (ChainPipe between function references)
6686            // e.g., $uppertrim := $trim ~> $uppercase
6687            // This creates a lambda that composes the functions.
6688            // But NOT for data ~> function, which should be evaluated immediately.
6689            // e.g., $result := data ~> $map($fn) should evaluate the pipe
6690            if let AstNode::Binary {
6691                op: BinaryOp::ChainPipe,
6692                lhs: chain_lhs,
6693                rhs: chain_rhs,
6694            } = rhs
6695            {
6696                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
6697                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
6698                let is_function_composition = match chain_lhs.as_ref() {
6699                    // LHS is a function reference like $trim or $sum
6700                    AstNode::Variable(name)
6701                        if self.is_builtin_function(name)
6702                            || self.context.lookup_lambda(name).is_some() =>
6703                    {
6704                        true
6705                    }
6706                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
6707                    AstNode::Binary {
6708                        op: BinaryOp::ChainPipe,
6709                        ..
6710                    } => true,
6711                    // A function call with placeholder creates a partial application
6712                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
6713                    AstNode::Function { args, .. }
6714                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
6715                    {
6716                        true
6717                    }
6718                    // Anything else (data, function calls, arrays, etc.) is not pure composition
6719                    _ => false,
6720                };
6721
6722                if is_function_composition {
6723                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
6724                    // The original chain is $trim ~> $uppercase (left-associative)
6725                    // We want to create: ($ ~> $trim) ~> $uppercase
6726                    let param_name = "$".to_string();
6727
6728                    // First create $ ~> $trim
6729                    let first_pipe = AstNode::Binary {
6730                        op: BinaryOp::ChainPipe,
6731                        lhs: Box::new(AstNode::Variable(param_name.clone())),
6732                        rhs: chain_lhs.clone(),
6733                    };
6734
6735                    // Then wrap with ~> $uppercase (or the rest of the chain)
6736                    let composed_body = AstNode::Binary {
6737                        op: BinaryOp::ChainPipe,
6738                        lhs: Box::new(first_pipe),
6739                        rhs: chain_rhs.clone(),
6740                    };
6741
6742                    let stored_lambda = StoredLambda {
6743                        params: vec![param_name],
6744                        body: composed_body,
6745                        compiled_body: None, // ChainPipe body is not compilable
6746                        signature: None,
6747                        captured_env: self.capture_current_environment(),
6748                        captured_data: Some(data.clone()),
6749                        thunk: false,
6750                    };
6751                    self.context.bind_lambda(var_name.clone(), stored_lambda);
6752
6753                    // Return a lambda marker value (include _lambda_id for later lookup)
6754                    let lambda_repr = JValue::lambda(
6755                        var_name.as_str(),
6756                        vec!["$".to_string()],
6757                        Some(var_name.clone()),
6758                        None::<String>,
6759                    );
6760                    return Ok(lambda_repr);
6761                }
6762                // If not function composition, fall through to normal evaluation below
6763            }
6764
6765            // Evaluate the RHS
6766            let value = self.evaluate_internal(rhs, data)?;
6767
6768            // If the value is a lambda, copy the stored lambda to the new variable name
6769            if let Some(stored) = self.lookup_lambda_from_value(&value) {
6770                self.context.bind_lambda(var_name.clone(), stored);
6771            }
6772
6773            // Bind even if undefined (null) so inner scopes can shadow outer variables
6774            self.context.bind(var_name, value.clone());
6775            return Ok(value);
6776        }
6777
6778        // Special handling for 'In' operator - check for array filtering
6779        // Must evaluate lhs first to determine if this is array filtering
6780        if op == BinaryOp::In {
6781            let left = self.evaluate_internal(lhs, data)?;
6782
6783            // Check if this is array filtering: array[predicate]
6784            if matches!(left, JValue::Array(_)) {
6785                // Try evaluating rhs in current context to see if it's a simple index
6786                let right_result = self.evaluate_internal(rhs, data);
6787
6788                if let Ok(JValue::Number(_)) = right_result {
6789                    // Simple numeric index: array[n]
6790                    return self.array_index(&left, &right_result.unwrap());
6791                } else {
6792                    // This is array filtering: array[predicate]
6793                    // Evaluate the predicate for each array item
6794                    return self.array_filter(lhs, rhs, &left, data);
6795                }
6796            }
6797        }
6798
6799        // Special handling for logical operators (short-circuit evaluation)
6800        if op == BinaryOp::And {
6801            let left = self.evaluate_internal(lhs, data)?;
6802            if !self.is_truthy(&left) {
6803                // Short-circuit: if left is falsy, return false without evaluating right
6804                return Ok(JValue::Bool(false));
6805            }
6806            let right = self.evaluate_internal(rhs, data)?;
6807            return Ok(JValue::Bool(self.is_truthy(&right)));
6808        }
6809
6810        if op == BinaryOp::Or {
6811            let left = self.evaluate_internal(lhs, data)?;
6812            if self.is_truthy(&left) {
6813                // Short-circuit: if left is truthy, return true without evaluating right
6814                return Ok(JValue::Bool(true));
6815            }
6816            let right = self.evaluate_internal(rhs, data)?;
6817            return Ok(JValue::Bool(self.is_truthy(&right)));
6818        }
6819
6820        // Check if operands are explicit null literals (vs undefined from variables)
6821        let left_is_explicit_null = matches!(lhs, AstNode::Null);
6822        let right_is_explicit_null = matches!(rhs, AstNode::Null);
6823
6824        // Standard evaluation: evaluate both operands
6825        let left = self.evaluate_internal(lhs, data)?;
6826        let right = self.evaluate_internal(rhs, data)?;
6827
6828        match op {
6829            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6830            BinaryOp::Subtract => {
6831                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6832            }
6833            BinaryOp::Multiply => {
6834                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6835            }
6836            BinaryOp::Divide => {
6837                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6838            }
6839            BinaryOp::Modulo => {
6840                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6841            }
6842
6843            // compiled_equal normalizes lazy operands (guarded, zero-cost when neither
6844            // side is lazy) so conversion failures raise instead of silently comparing
6845            // unequal.
6846            BinaryOp::Equal => compiled_equal(&left, &right),
6847            BinaryOp::NotEqual => match compiled_equal(&left, &right)? {
6848                JValue::Bool(b) => Ok(JValue::Bool(!b)),
6849                other => Ok(other),
6850            },
6851            BinaryOp::LessThan => {
6852                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6853            }
6854            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6855                &left,
6856                &right,
6857                left_is_explicit_null,
6858                right_is_explicit_null,
6859            ),
6860            BinaryOp::GreaterThan => {
6861                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6862            }
6863            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6864                &left,
6865                &right,
6866                left_is_explicit_null,
6867                right_is_explicit_null,
6868            ),
6869
6870            // And/Or handled above with short-circuit evaluation
6871            BinaryOp::And | BinaryOp::Or => unreachable!(),
6872
6873            BinaryOp::Concatenate => self.concatenate(&left, &right),
6874            BinaryOp::Range => self.range(&left, &right),
6875            BinaryOp::In => self.in_operator(&left, &right),
6876
6877            // Focus binding: should be resolved by ast_transform pass (Task 2)
6878            BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6879                "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6880            )),
6881
6882            // Index binding: should be resolved by ast_transform pass (Task 4,
6883            // which retired the dedicated AstNode::IndexBind variant in favor
6884            // of this generic Binary marker, mirroring FocusBind above)
6885            BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6886                "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6887            )),
6888
6889            // These operators are all handled as special cases earlier in evaluate_binary_op
6890            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6891                unreachable!()
6892            }
6893        }
6894    }
6895
6896    /// Evaluate a unary operation
6897    fn evaluate_unary_op(
6898        &mut self,
6899        op: crate::ast::UnaryOp,
6900        operand: &AstNode,
6901        data: &JValue,
6902    ) -> Result<JValue, EvaluatorError> {
6903        use crate::ast::UnaryOp;
6904
6905        let value = self.evaluate_internal(operand, data)?;
6906
6907        match op {
6908            UnaryOp::Negate => match value {
6909                // undefined returns undefined
6910                JValue::Null | JValue::Undefined => Ok(JValue::Null),
6911                JValue::Number(n) => Ok(JValue::Number(-n)),
6912                _ => Err(EvaluatorError::TypeError(
6913                    "D1002: Cannot negate non-number value".to_string(),
6914                )),
6915            },
6916            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6917        }
6918    }
6919
6920    /// Try to fuse an aggregate function call with its Path argument.
6921    /// Handles patterns like:
6922    /// - $sum(arr.field) → iterate arr, extract field, accumulate
6923    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6924    ///
6925    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6926    fn try_fused_aggregate(
6927        &mut self,
6928        name: &str,
6929        arg: &AstNode,
6930        data: &JValue,
6931    ) -> Result<Option<JValue>, EvaluatorError> {
6932        // Only applies to numeric aggregates
6933        if !matches!(name, "sum" | "max" | "min" | "average") {
6934            return Ok(None);
6935        }
6936
6937        // Argument must be a Path
6938        let AstNode::Path { steps } = arg else {
6939            return Ok(None);
6940        };
6941
6942        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6943        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6944        if steps.len() != 2 {
6945            return Ok(None);
6946        }
6947
6948        // Last step must be a simple Name (the field to extract)
6949        let field_step = &steps[1];
6950        if !field_step.stages.is_empty() {
6951            return Ok(None);
6952        }
6953        let AstNode::Name(extract_field) = &field_step.node else {
6954            return Ok(None);
6955        };
6956
6957        // First step: Name with optional filter stage
6958        let arr_step = &steps[0];
6959        let AstNode::Name(arr_name) = &arr_step.node else {
6960            return Ok(None);
6961        };
6962
6963        // Get the source array from data
6964        let arr = match data {
6965            JValue::Object(obj) => match obj.get(arr_name) {
6966                Some(JValue::Array(arr)) => arr,
6967                _ => return Ok(None),
6968            },
6969            _ => return Ok(None),
6970        };
6971
6972        // Check for filter stage — try CompiledExpr for the predicate
6973        let filter_compiled = match arr_step.stages.as_slice() {
6974            [] => None,
6975            [Stage::Filter(pred)] => try_compile_expr(pred),
6976            _ => return Ok(None),
6977        };
6978        // If filter stage exists but wasn't compilable, bail out
6979        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6980            return Ok(None);
6981        }
6982
6983        // Build shape cache for the array
6984        let shape = arr.first().and_then(build_shape_cache);
6985
6986        // Fused iteration: filter (optional) + extract + aggregate
6987        let mut total = 0.0f64;
6988        let mut count = 0usize;
6989        let mut max_val = f64::NEG_INFINITY;
6990        let mut min_val = f64::INFINITY;
6991        let mut has_any = false;
6992
6993        for item in arr.iter() {
6994            // Apply compiled filter if present
6995            if let Some(ref compiled) = filter_compiled {
6996                let result = if let Some(ref s) = shape {
6997                    eval_compiled_shaped(compiled, item, None, s, &self.options, self.start_time)?
6998                } else {
6999                    eval_compiled(compiled, item, None, &self.options, self.start_time)?
7000                };
7001                if !compiled_is_truthy(&result) {
7002                    continue;
7003                }
7004            }
7005
7006            // Extract field value
7007            let val = match item {
7008                JValue::Object(obj) => match obj.get(extract_field) {
7009                    Some(JValue::Number(n)) => *n,
7010                    Some(_) | None => continue, // Skip non-numeric / missing
7011                },
7012                _ => continue,
7013            };
7014
7015            has_any = true;
7016            match name {
7017                "sum" => total += val,
7018                "max" => max_val = max_val.max(val),
7019                "min" => min_val = min_val.min(val),
7020                "average" => {
7021                    total += val;
7022                    count += 1;
7023                }
7024                _ => unreachable!(),
7025            }
7026        }
7027
7028        if !has_any {
7029            return Ok(Some(match name {
7030                "sum" => JValue::from(0i64),
7031                "average" | "max" | "min" => JValue::Null,
7032                _ => unreachable!(),
7033            }));
7034        }
7035
7036        Ok(Some(match name {
7037            "sum" => JValue::Number(total),
7038            "max" => JValue::Number(max_val),
7039            "min" => JValue::Number(min_val),
7040            "average" => JValue::Number(total / count as f64),
7041            _ => unreachable!(),
7042        }))
7043    }
7044
7045    /// Evaluate a function call
7046    fn evaluate_function_call(
7047        &mut self,
7048        name: &str,
7049        args: &[AstNode],
7050        is_builtin: bool,
7051        data: &JValue,
7052    ) -> Result<JValue, EvaluatorError> {
7053        use crate::functions;
7054
7055        // Check for partial application (any argument is a Placeholder)
7056        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
7057        if has_placeholder {
7058            return self.create_partial_application(name, args, is_builtin, data);
7059        }
7060
7061        // FIRST check if this variable holds a function value (lambda or builtin reference)
7062        // This is critical for:
7063        // 1. Allowing function parameters to shadow stored lambdas
7064        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
7065        // 2. Calling built-in functions passed as parameters
7066        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
7067        if let Some(value) = self.context.lookup(name).cloned() {
7068            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
7069                let mut evaluated_args = Vec::with_capacity(args.len());
7070                for arg in args {
7071                    evaluated_args.push(self.evaluate_internal(arg, data)?);
7072                }
7073                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
7074            }
7075            if let JValue::Builtin { name: builtin_name } = &value {
7076                // This is a built-in function reference (e.g., $f bound to $sum)
7077                let mut evaluated_args = Vec::with_capacity(args.len());
7078                for arg in args {
7079                    evaluated_args.push(self.evaluate_internal(arg, data)?);
7080                }
7081                return self.call_builtin_with_values(builtin_name, &evaluated_args);
7082            }
7083        }
7084
7085        // THEN check if this is a stored lambda (user-defined function by name)
7086        // This only applies if not shadowed by a binding above
7087        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
7088            let mut evaluated_args = Vec::with_capacity(args.len());
7089            for arg in args {
7090                evaluated_args.push(self.evaluate_internal(arg, data)?);
7091            }
7092            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
7093        }
7094
7095        // If the function was called without $ prefix and it's not a stored lambda,
7096        // it's an error (unknown function without $ prefix)
7097        if !is_builtin && name != "__lambda__" {
7098            return Err(EvaluatorError::ReferenceError(format!(
7099                "Unknown function: {}",
7100                name
7101            )));
7102        }
7103
7104        // Special handling for $exists function
7105        // It needs to know if the argument is explicit null vs undefined
7106        if name == "exists" && args.len() == 1 {
7107            let arg = &args[0];
7108
7109            // Check if it's an explicit null literal
7110            if matches!(arg, AstNode::Null) {
7111                return Ok(JValue::Bool(true)); // Explicit null exists
7112            }
7113
7114            // Check if it's a function reference
7115            if let AstNode::Variable(var_name) = arg {
7116                if self.is_builtin_function(var_name) {
7117                    return Ok(JValue::Bool(true)); // Built-in function exists
7118                }
7119
7120                // Check if it's a stored lambda
7121                if self.context.lookup_lambda(var_name).is_some() {
7122                    return Ok(JValue::Bool(true)); // Lambda exists
7123                }
7124
7125                // Check if the variable is defined
7126                if let Some(val) = self.context.lookup(var_name) {
7127                    // A variable bound to the undefined marker doesn't "exist"
7128                    if val.is_undefined() {
7129                        return Ok(JValue::Bool(false));
7130                    }
7131                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
7132                } else {
7133                    return Ok(JValue::Bool(false)); // Variable is undefined
7134                }
7135            }
7136
7137            // For other expressions, evaluate and check if non-null/non-undefined
7138            let value = self.evaluate_internal(arg, data)?;
7139            return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
7140        }
7141
7142        // Check if any arguments are undefined variables or undefined paths
7143        // Functions like $not() should return undefined when given undefined values
7144        for arg in args {
7145            // Check for undefined variable (e.g., $undefined_var)
7146            if let AstNode::Variable(var_name) = arg {
7147                // Skip built-in function names - they're function references, not undefined variables
7148                if !var_name.is_empty()
7149                    && !self.is_builtin_function(var_name)
7150                    && self.context.lookup(var_name).is_none()
7151                {
7152                    // Undefined variable - for functions that should propagate undefined
7153                    if propagates_undefined(name) {
7154                        return Ok(JValue::Null); // Return undefined
7155                    }
7156                }
7157            }
7158            // Check for simple field name (e.g., blah) that evaluates to undefined
7159            if let AstNode::Name(field_name) = arg {
7160                let field_exists = matches!(data, JValue::Object(obj) if obj.contains_key(field_name))
7161                    || {
7162                        #[cfg(feature = "python")]
7163                        {
7164                            matches!(data, JValue::LazyPyDict(l) if l.contains_field(field_name))
7165                        }
7166                        #[cfg(not(feature = "python"))]
7167                        {
7168                            false
7169                        }
7170                    };
7171                if !field_exists && propagates_undefined(name) {
7172                    return Ok(JValue::Null);
7173                }
7174            }
7175            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
7176            // Field accesses are represented as AstNode::Path. String literals should never
7177            // be checked for undefined propagation.
7178            // Check for Path expressions that evaluate to undefined
7179            if let AstNode::Path { steps } = arg {
7180                // For paths that evaluate to null, we need to determine if it's because:
7181                // 1. A field doesn't exist (undefined) - should propagate as undefined
7182                // 2. A field exists with value null - should throw T0410
7183                //
7184                // We can distinguish these by checking if the path is accessing a field
7185                // that doesn't exist on an object vs one that has an explicit null value.
7186                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
7187                    // Path evaluated to null - now check if it's truly undefined
7188                    // For single-step paths, check if the field exists
7189                    if steps.len() == 1 {
7190                        // Get field name - could be Name (identifier) or String (quoted)
7191                        let field_name = match &steps[0].node {
7192                            AstNode::Name(n) => Some(n.as_str()),
7193                            AstNode::String(s) => Some(s.as_str()),
7194                            _ => None,
7195                        };
7196                        if let Some(field) = field_name {
7197                            match data {
7198                                JValue::Object(obj) => {
7199                                    if !obj.contains_key(field) {
7200                                        // Field doesn't exist - return undefined
7201                                        if propagates_undefined(name) {
7202                                            return Ok(JValue::Null);
7203                                        }
7204                                    }
7205                                    // Field exists with value null - continue to throw T0410
7206                                }
7207                                // Trying to access field on null data - return undefined
7208                                JValue::Null if propagates_undefined(name) => {
7209                                    return Ok(JValue::Null);
7210                                }
7211                                _ => {}
7212                            }
7213                        }
7214                    }
7215                    // For multi-step paths, check if any intermediate step failed
7216                    else if steps.len() > 1 {
7217                        // Evaluate each step to find where it breaks
7218                        let mut current = data;
7219                        let mut failed_due_to_missing_field = false;
7220
7221                        for (i, step) in steps.iter().enumerate() {
7222                            if let AstNode::Name(field_name) = &step.node {
7223                                match current {
7224                                    JValue::Object(obj) => {
7225                                        if let Some(val) = obj.get(field_name) {
7226                                            current = val;
7227                                        } else {
7228                                            // Field doesn't exist
7229                                            failed_due_to_missing_field = true;
7230                                            break;
7231                                        }
7232                                    }
7233                                    JValue::Array(_) => {
7234                                        // Array access - evaluate normally
7235                                        break;
7236                                    }
7237                                    JValue::Null => {
7238                                        // Hit null in the middle of the path
7239                                        if i > 0 {
7240                                            // Previous field had null value - not undefined
7241                                            failed_due_to_missing_field = false;
7242                                        }
7243                                        break;
7244                                    }
7245                                    _ => break,
7246                                }
7247                            }
7248                        }
7249
7250                        if failed_due_to_missing_field && propagates_undefined(name) {
7251                            return Ok(JValue::Null);
7252                        }
7253                    }
7254                }
7255            }
7256        }
7257
7258        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
7259        // try to fuse filter+extract+aggregate into a single pass.
7260        if args.len() == 1 {
7261            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
7262                return Ok(result);
7263            }
7264        }
7265
7266        let mut evaluated_args = Vec::with_capacity(args.len());
7267        for arg in args {
7268            evaluated_args.push(self.evaluate_internal(arg, data)?);
7269        }
7270
7271        // JSONata feature: when a function is called with no arguments but expects
7272        // at least one, use the current context value (data) as the implicit first argument
7273        // This also applies when functions expecting N arguments receive N-1 arguments,
7274        // in which case the context value becomes the first argument
7275        let context_functions_zero_arg = [
7276            "string",
7277            "number",
7278            "boolean",
7279            "uppercase",
7280            "lowercase",
7281            "fromMillis",
7282        ];
7283        let context_functions_missing_first = [
7284            "substringBefore",
7285            "substringAfter",
7286            "contains",
7287            "split",
7288            "replace",
7289        ];
7290
7291        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
7292            // Use the current context value as the implicit argument
7293            evaluated_args.push(data.clone());
7294        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
7295            // These functions expect 2+ arguments, but received 1
7296            // Only insert context if it's a compatible type (string for string functions)
7297            // Otherwise, let the function throw T0411 for wrong argument count
7298            if matches!(data, JValue::String(_)) {
7299                evaluated_args.insert(0, data.clone());
7300            }
7301        }
7302
7303        // Special handling for $string() with no explicit arguments
7304        // After context insertion, check if the argument is null (undefined context)
7305        if name == "string"
7306            && args.is_empty()
7307            && !evaluated_args.is_empty()
7308            && evaluated_args[0].is_null()
7309        {
7310            // Context was null/undefined, so return undefined
7311            return Ok(JValue::Null);
7312        }
7313
7314        #[cfg(feature = "python")]
7315        for arg in evaluated_args.iter_mut() {
7316            if matches!(arg, JValue::LazyPyDict(_)) {
7317                *arg = normalize_lazy(arg)?;
7318            }
7319        }
7320
7321        match name {
7322            "string" => {
7323                if evaluated_args.len() > 2 {
7324                    return Err(EvaluatorError::EvaluationError(
7325                        "string() takes at most 2 arguments".to_string(),
7326                    ));
7327                }
7328
7329                let prettify = if evaluated_args.len() == 2 {
7330                    match &evaluated_args[1] {
7331                        JValue::Bool(b) => Some(*b),
7332                        _ => {
7333                            return Err(EvaluatorError::TypeError(
7334                                "string() prettify parameter must be a boolean".to_string(),
7335                            ))
7336                        }
7337                    }
7338                } else {
7339                    None
7340                };
7341
7342                Ok(functions::string::string(&evaluated_args[0], prettify)?)
7343            }
7344            "length" => {
7345                if evaluated_args.len() != 1 {
7346                    return Err(EvaluatorError::EvaluationError(
7347                        "length() requires exactly 1 argument".to_string(),
7348                    ));
7349                }
7350                if evaluated_args[0].is_undefined() {
7351                    return Ok(JValue::Undefined);
7352                }
7353                match &evaluated_args[0] {
7354                    JValue::String(s) => Ok(functions::string::length(s)?),
7355                    _ => Err(EvaluatorError::TypeError(
7356                        "T0410: Argument 1 of function length does not match function signature"
7357                            .to_string(),
7358                    )),
7359                }
7360            }
7361            "uppercase" => {
7362                if evaluated_args.len() != 1 {
7363                    return Err(EvaluatorError::EvaluationError(
7364                        "uppercase() requires exactly 1 argument".to_string(),
7365                    ));
7366                }
7367                if evaluated_args[0].is_undefined() {
7368                    return Ok(JValue::Undefined);
7369                }
7370                match &evaluated_args[0] {
7371                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
7372                    _ => Err(EvaluatorError::TypeError(
7373                        "T0410: Argument 1 of function uppercase does not match function signature"
7374                            .to_string(),
7375                    )),
7376                }
7377            }
7378            "lowercase" => {
7379                if evaluated_args.len() != 1 {
7380                    return Err(EvaluatorError::EvaluationError(
7381                        "lowercase() requires exactly 1 argument".to_string(),
7382                    ));
7383                }
7384                if evaluated_args[0].is_undefined() {
7385                    return Ok(JValue::Undefined);
7386                }
7387                match &evaluated_args[0] {
7388                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
7389                    _ => Err(EvaluatorError::TypeError(
7390                        "T0410: Argument 1 of function lowercase does not match function signature"
7391                            .to_string(),
7392                    )),
7393                }
7394            }
7395            "number" => {
7396                if evaluated_args.is_empty() {
7397                    return Err(EvaluatorError::EvaluationError(
7398                        "number() requires at least 1 argument".to_string(),
7399                    ));
7400                }
7401                if evaluated_args.len() > 1 {
7402                    return Err(EvaluatorError::TypeError(
7403                        "T0410: Argument 2 of function number does not match function signature"
7404                            .to_string(),
7405                    ));
7406                }
7407                if evaluated_args[0].is_undefined() {
7408                    return Ok(JValue::Undefined);
7409                }
7410                Ok(functions::numeric::number(&evaluated_args[0])?)
7411            }
7412            "sum" => {
7413                if evaluated_args.len() != 1 {
7414                    return Err(EvaluatorError::EvaluationError(
7415                        "sum() requires exactly 1 argument".to_string(),
7416                    ));
7417                }
7418                // Return undefined if argument is undefined
7419                if evaluated_args[0].is_undefined() {
7420                    return Ok(JValue::Undefined);
7421                }
7422                match &evaluated_args[0] {
7423                    JValue::Null => Ok(JValue::Null),
7424                    JValue::Array(arr) => {
7425                        // Use zero-clone iterator-based sum
7426                        Ok(aggregation::sum(arr)?)
7427                    }
7428                    // Non-array values: extract number directly
7429                    JValue::Number(n) => Ok(JValue::Number(*n)),
7430                    other => Ok(functions::numeric::sum(&[other.clone()])?),
7431                }
7432            }
7433            "count" => {
7434                if evaluated_args.len() != 1 {
7435                    return Err(EvaluatorError::EvaluationError(
7436                        "count() requires exactly 1 argument".to_string(),
7437                    ));
7438                }
7439                // Return 0 if argument is undefined
7440                if evaluated_args[0].is_undefined() {
7441                    return Ok(JValue::from(0i64));
7442                }
7443                match &evaluated_args[0] {
7444                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
7445                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
7446                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
7447                }
7448            }
7449            "substring" => {
7450                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7451                    return Err(EvaluatorError::EvaluationError(
7452                        "substring() requires 2 or 3 arguments".to_string(),
7453                    ));
7454                }
7455                if evaluated_args[0].is_undefined() {
7456                    return Ok(JValue::Undefined);
7457                }
7458                match (&evaluated_args[0], &evaluated_args[1]) {
7459                    (JValue::String(s), JValue::Number(start)) => {
7460                        let length = if evaluated_args.len() == 3 {
7461                            match &evaluated_args[2] {
7462                                JValue::Number(l) => Some(*l as i64),
7463                                _ => return Err(EvaluatorError::TypeError(
7464                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
7465                                )),
7466                            }
7467                        } else {
7468                            None
7469                        };
7470                        Ok(functions::string::substring(s, *start as i64, length)?)
7471                    }
7472                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7473                        "T0410: Argument 2 of function substring does not match function signature"
7474                            .to_string(),
7475                    )),
7476                    _ => Err(EvaluatorError::TypeError(
7477                        "T0410: Argument 1 of function substring does not match function signature"
7478                            .to_string(),
7479                    )),
7480                }
7481            }
7482            "substringBefore" => {
7483                if evaluated_args.len() != 2 {
7484                    return Err(EvaluatorError::TypeError(
7485                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
7486                    ));
7487                }
7488                if evaluated_args[0].is_undefined() {
7489                    return Ok(JValue::Undefined);
7490                }
7491                match (&evaluated_args[0], &evaluated_args[1]) {
7492                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
7493                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7494                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
7495                    )),
7496                    _ => Err(EvaluatorError::TypeError(
7497                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
7498                    )),
7499                }
7500            }
7501            "substringAfter" => {
7502                if evaluated_args.len() != 2 {
7503                    return Err(EvaluatorError::TypeError(
7504                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
7505                    ));
7506                }
7507                if evaluated_args[0].is_undefined() {
7508                    return Ok(JValue::Undefined);
7509                }
7510                match (&evaluated_args[0], &evaluated_args[1]) {
7511                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
7512                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7513                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
7514                    )),
7515                    _ => Err(EvaluatorError::TypeError(
7516                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
7517                    )),
7518                }
7519            }
7520            "pad" => {
7521                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7522                    return Err(EvaluatorError::EvaluationError(
7523                        "pad() requires 2 or 3 arguments".to_string(),
7524                    ));
7525                }
7526
7527                // First argument: string to pad
7528                let string = match &evaluated_args[0] {
7529                    JValue::String(s) => s.clone(),
7530                    JValue::Null => return Ok(JValue::Null),
7531                    JValue::Undefined => return Ok(JValue::Undefined),
7532                    _ => {
7533                        return Err(EvaluatorError::TypeError(
7534                            "pad() first argument must be a string".to_string(),
7535                        ))
7536                    }
7537                };
7538
7539                // Second argument: width (negative = left pad, positive = right pad)
7540                let width = match &evaluated_args.get(1) {
7541                    Some(JValue::Number(n)) => *n as i32,
7542                    _ => {
7543                        return Err(EvaluatorError::TypeError(
7544                            "pad() second argument must be a number".to_string(),
7545                        ))
7546                    }
7547                };
7548
7549                // Third argument: padding string (optional, defaults to space)
7550                let pad_string = match evaluated_args.get(2) {
7551                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
7552                    _ => Rc::from(" "),
7553                };
7554
7555                let abs_width = width.unsigned_abs() as usize;
7556                // Count Unicode characters (code points), not bytes
7557                let char_count = string.chars().count();
7558
7559                if char_count >= abs_width {
7560                    // String is already long enough
7561                    return Ok(JValue::string(string));
7562                }
7563
7564                let padding_needed = abs_width - char_count;
7565
7566                let pad_chars: Vec<char> = pad_string.chars().collect();
7567                let mut padding = String::with_capacity(padding_needed);
7568                for i in 0..padding_needed {
7569                    padding.push(pad_chars[i % pad_chars.len()]);
7570                }
7571
7572                let result = if width < 0 {
7573                    // Left pad (negative width)
7574                    format!("{}{}", padding, string)
7575                } else {
7576                    // Right pad (positive width)
7577                    format!("{}{}", string, padding)
7578                };
7579
7580                Ok(JValue::string(result))
7581            }
7582
7583            "trim" => {
7584                if evaluated_args.is_empty() {
7585                    return Ok(JValue::Null); // undefined
7586                }
7587                if evaluated_args.len() != 1 {
7588                    return Err(EvaluatorError::EvaluationError(
7589                        "trim() requires at most 1 argument".to_string(),
7590                    ));
7591                }
7592                match &evaluated_args[0] {
7593                    JValue::Null => Ok(JValue::Null),
7594                    JValue::String(s) => Ok(functions::string::trim(s)?),
7595                    _ => Err(EvaluatorError::TypeError(
7596                        "trim() requires a string argument".to_string(),
7597                    )),
7598                }
7599            }
7600            "contains" => {
7601                if evaluated_args.len() != 2 {
7602                    return Err(EvaluatorError::EvaluationError(
7603                        "contains() requires exactly 2 arguments".to_string(),
7604                    ));
7605                }
7606                if evaluated_args[0].is_null() {
7607                    return Ok(JValue::Null);
7608                }
7609                if evaluated_args[0].is_undefined() {
7610                    return Ok(JValue::Undefined);
7611                }
7612                match &evaluated_args[0] {
7613                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
7614                    _ => Err(EvaluatorError::TypeError(
7615                        "contains() requires a string as the first argument".to_string(),
7616                    )),
7617                }
7618            }
7619            "split" => {
7620                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7621                    return Err(EvaluatorError::EvaluationError(
7622                        "split() requires 2 or 3 arguments".to_string(),
7623                    ));
7624                }
7625                if evaluated_args[0].is_null() {
7626                    return Ok(JValue::Null);
7627                }
7628                if evaluated_args[0].is_undefined() {
7629                    return Ok(JValue::Undefined);
7630                }
7631                match &evaluated_args[0] {
7632                    JValue::String(s) => {
7633                        let limit = if evaluated_args.len() == 3 {
7634                            match &evaluated_args[2] {
7635                                JValue::Number(n) => {
7636                                    let f = *n;
7637                                    // Negative limit is an error
7638                                    if f < 0.0 {
7639                                        return Err(EvaluatorError::EvaluationError(
7640                                            "D3020: Third argument of split function must be a positive number".to_string(),
7641                                        ));
7642                                    }
7643                                    // Floor the value for non-integer limits
7644                                    Some(f.floor() as usize)
7645                                }
7646                                _ => {
7647                                    return Err(EvaluatorError::TypeError(
7648                                        "split() limit must be a number".to_string(),
7649                                    ))
7650                                }
7651                            }
7652                        } else {
7653                            None
7654                        };
7655                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
7656                    }
7657                    _ => Err(EvaluatorError::TypeError(
7658                        "split() requires a string as the first argument".to_string(),
7659                    )),
7660                }
7661            }
7662            "join" => {
7663                // Special case: if first arg is undefined, return undefined
7664                // But if separator (2nd arg) is undefined, use empty string (default)
7665                if evaluated_args.is_empty() {
7666                    return Err(EvaluatorError::TypeError(
7667                        "T0410: Argument 1 of function $join does not match function signature"
7668                            .to_string(),
7669                    ));
7670                }
7671                if evaluated_args[0].is_null() {
7672                    return Ok(JValue::Null);
7673                }
7674                if evaluated_args[0].is_undefined() {
7675                    return Ok(JValue::Undefined);
7676                }
7677
7678                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
7679                // The signature handles coercion and validation
7680                use crate::signature::Signature;
7681
7682                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
7683                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7684                })?;
7685
7686                let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
7687                    Ok(args) => args,
7688                    Err(crate::signature::SignatureError::UndefinedArgument) => {
7689                        // This can happen if the separator is undefined
7690                        // In that case, just validate the first arg and use default separator
7691                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
7692                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7693                        })?;
7694
7695                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
7696                            Ok(args) => args,
7697                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
7698                                index,
7699                                expected,
7700                            }) => {
7701                                return Err(EvaluatorError::TypeError(format!(
7702                                    "T0412: Argument {} of function $join must be an array of {}",
7703                                    index, expected
7704                                )));
7705                            }
7706                            Err(e) => {
7707                                return Err(EvaluatorError::TypeError(format!(
7708                                    "Signature validation failed: {}",
7709                                    e
7710                                )));
7711                            }
7712                        }
7713                    }
7714                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
7715                        index,
7716                        expected,
7717                    }) => {
7718                        return Err(EvaluatorError::TypeError(
7719                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
7720                        ));
7721                    }
7722                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
7723                        index,
7724                        expected,
7725                    }) => {
7726                        return Err(EvaluatorError::TypeError(format!(
7727                            "T0412: Argument {} of function $join must be an array of {}",
7728                            index, expected
7729                        )));
7730                    }
7731                    Err(e) => {
7732                        return Err(EvaluatorError::TypeError(format!(
7733                            "Signature validation failed: {}",
7734                            e
7735                        )));
7736                    }
7737                };
7738
7739                // After coercion, first arg is guaranteed to be an array of strings
7740                match &coerced_args[0] {
7741                    JValue::Array(arr) => {
7742                        let separator = if coerced_args.len() == 2 {
7743                            match &coerced_args[1] {
7744                                JValue::String(s) => Some(&**s),
7745                                JValue::Null => None, // Undefined separator -> use empty string
7746                                _ => None,            // Signature should have validated this
7747                            }
7748                        } else {
7749                            None // No separator provided -> use empty string
7750                        };
7751                        Ok(functions::string::join(arr, separator)?)
7752                    }
7753                    JValue::Null => Ok(JValue::Null),
7754                    _ => unreachable!("Signature validation should ensure array type"),
7755                }
7756            }
7757            "replace" => {
7758                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
7759                    return Err(EvaluatorError::EvaluationError(
7760                        "replace() requires 3 or 4 arguments".to_string(),
7761                    ));
7762                }
7763                if evaluated_args[0].is_null() {
7764                    return Ok(JValue::Null);
7765                }
7766                if evaluated_args[0].is_undefined() {
7767                    return Ok(JValue::Undefined);
7768                }
7769
7770                // Check if replacement (3rd arg) is a function/lambda
7771                let replacement_is_lambda = matches!(
7772                    evaluated_args[2],
7773                    JValue::Lambda { .. } | JValue::Builtin { .. }
7774                );
7775
7776                if replacement_is_lambda {
7777                    // Lambda replacement mode
7778                    return self.replace_with_lambda(
7779                        &evaluated_args[0],
7780                        &evaluated_args[1],
7781                        &evaluated_args[2],
7782                        if evaluated_args.len() == 4 {
7783                            Some(&evaluated_args[3])
7784                        } else {
7785                            None
7786                        },
7787                        data,
7788                    );
7789                }
7790
7791                // String replacement mode
7792                match (&evaluated_args[0], &evaluated_args[2]) {
7793                    (JValue::String(s), JValue::String(replacement)) => {
7794                        let limit = if evaluated_args.len() == 4 {
7795                            match &evaluated_args[3] {
7796                                JValue::Number(n) => {
7797                                    let lim_f64 = *n;
7798                                    if lim_f64 < 0.0 {
7799                                        return Err(EvaluatorError::EvaluationError(format!(
7800                                            "D3011: Limit must be non-negative, got {}",
7801                                            lim_f64
7802                                        )));
7803                                    }
7804                                    Some(lim_f64 as usize)
7805                                }
7806                                _ => {
7807                                    return Err(EvaluatorError::TypeError(
7808                                        "replace() limit must be a number".to_string(),
7809                                    ))
7810                                }
7811                            }
7812                        } else {
7813                            None
7814                        };
7815                        Ok(functions::string::replace(
7816                            s,
7817                            &evaluated_args[1],
7818                            replacement,
7819                            limit,
7820                        )?)
7821                    }
7822                    _ => Err(EvaluatorError::TypeError(
7823                        "replace() requires string arguments".to_string(),
7824                    )),
7825                }
7826            }
7827            "match" => {
7828                // $match(str, pattern [, limit])
7829                // Returns array of match objects for regex matches or custom matcher function
7830                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7831                    return Err(EvaluatorError::EvaluationError(
7832                        "match() requires 1 to 3 arguments".to_string(),
7833                    ));
7834                }
7835                if evaluated_args[0].is_null() {
7836                    return Ok(JValue::Null);
7837                }
7838                if evaluated_args[0].is_undefined() {
7839                    return Ok(JValue::Undefined);
7840                }
7841
7842                let s = match &evaluated_args[0] {
7843                    JValue::String(s) => s.clone(),
7844                    _ => {
7845                        return Err(EvaluatorError::TypeError(
7846                            "match() first argument must be a string".to_string(),
7847                        ))
7848                    }
7849                };
7850
7851                // Get optional limit
7852                let limit = if evaluated_args.len() == 3 {
7853                    match &evaluated_args[2] {
7854                        JValue::Number(n) => Some(*n as usize),
7855                        JValue::Null => None,
7856                        _ => {
7857                            return Err(EvaluatorError::TypeError(
7858                                "match() limit must be a number".to_string(),
7859                            ))
7860                        }
7861                    }
7862                } else {
7863                    None
7864                };
7865
7866                // Check if second argument is a custom matcher function (lambda)
7867                let pattern_value = evaluated_args.get(1);
7868                let is_custom_matcher = pattern_value.is_some_and(|val| {
7869                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7870                });
7871
7872                if is_custom_matcher {
7873                    // Custom matcher function support
7874                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7875                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
7876                }
7877
7878                // Get regex pattern from second argument
7879                let (pattern, flags) = match pattern_value {
7880                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7881                        EvaluatorError::TypeError(
7882                            "match() second argument must be a regex pattern or matcher function"
7883                                .to_string(),
7884                        )
7885                    })?,
7886                    None => (".*".to_string(), "".to_string()),
7887                };
7888
7889                // Build regex
7890                let is_global = flags.contains('g');
7891                let regex_pattern = if flags.contains('i') {
7892                    format!("(?i){}", pattern)
7893                } else {
7894                    pattern.clone()
7895                };
7896
7897                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
7898                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7899                })?;
7900
7901                let mut results = Vec::new();
7902                let mut count = 0;
7903
7904                for caps in re.captures_iter(&s) {
7905                    if let Some(lim) = limit {
7906                        if count >= lim {
7907                            break;
7908                        }
7909                    }
7910
7911                    let full_match = caps.get(0).unwrap();
7912                    let mut match_obj = IndexMap::new();
7913                    match_obj.insert(
7914                        "match".to_string(),
7915                        JValue::string(full_match.as_str().to_string()),
7916                    );
7917                    match_obj.insert(
7918                        "index".to_string(),
7919                        JValue::Number(full_match.start() as f64),
7920                    );
7921
7922                    // Collect capture groups
7923                    let mut groups: Vec<JValue> = Vec::new();
7924                    for i in 1..caps.len() {
7925                        if let Some(group) = caps.get(i) {
7926                            groups.push(JValue::string(group.as_str().to_string()));
7927                        } else {
7928                            groups.push(JValue::Null);
7929                        }
7930                    }
7931                    if !groups.is_empty() {
7932                        match_obj.insert("groups".to_string(), JValue::array(groups));
7933                    }
7934
7935                    results.push(JValue::object(match_obj));
7936                    count += 1;
7937
7938                    // If not global, only return first match
7939                    if !is_global {
7940                        break;
7941                    }
7942                }
7943
7944                if results.is_empty() {
7945                    Ok(JValue::Null)
7946                } else if results.len() == 1 && !is_global {
7947                    // Single match (non-global) returns the match object directly
7948                    Ok(results.into_iter().next().unwrap())
7949                } else {
7950                    Ok(JValue::array(results))
7951                }
7952            }
7953            "max" => {
7954                if evaluated_args.len() != 1 {
7955                    return Err(EvaluatorError::EvaluationError(
7956                        "max() requires exactly 1 argument".to_string(),
7957                    ));
7958                }
7959                // Check for undefined
7960                if evaluated_args[0].is_undefined() {
7961                    return Ok(JValue::Undefined);
7962                }
7963                match &evaluated_args[0] {
7964                    JValue::Null => Ok(JValue::Null),
7965                    JValue::Array(arr) => {
7966                        // Use zero-clone iterator-based max
7967                        Ok(aggregation::max(arr)?)
7968                    }
7969                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7970                    _ => Err(EvaluatorError::TypeError(
7971                        "max() requires an array or number argument".to_string(),
7972                    )),
7973                }
7974            }
7975            "min" => {
7976                if evaluated_args.len() != 1 {
7977                    return Err(EvaluatorError::EvaluationError(
7978                        "min() requires exactly 1 argument".to_string(),
7979                    ));
7980                }
7981                // Check for undefined
7982                if evaluated_args[0].is_undefined() {
7983                    return Ok(JValue::Undefined);
7984                }
7985                match &evaluated_args[0] {
7986                    JValue::Null => Ok(JValue::Null),
7987                    JValue::Array(arr) => {
7988                        // Use zero-clone iterator-based min
7989                        Ok(aggregation::min(arr)?)
7990                    }
7991                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7992                    _ => Err(EvaluatorError::TypeError(
7993                        "min() requires an array or number argument".to_string(),
7994                    )),
7995                }
7996            }
7997            "average" => {
7998                if evaluated_args.len() != 1 {
7999                    return Err(EvaluatorError::EvaluationError(
8000                        "average() requires exactly 1 argument".to_string(),
8001                    ));
8002                }
8003                // Return undefined if argument is undefined
8004                if evaluated_args[0].is_undefined() {
8005                    return Ok(JValue::Undefined);
8006                }
8007                match &evaluated_args[0] {
8008                    JValue::Null => Ok(JValue::Null),
8009                    JValue::Array(arr) => {
8010                        // Use zero-clone iterator-based average
8011                        Ok(aggregation::average(arr)?)
8012                    }
8013                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
8014                    _ => Err(EvaluatorError::TypeError(
8015                        "average() requires an array or number argument".to_string(),
8016                    )),
8017                }
8018            }
8019            "abs" => {
8020                if evaluated_args.len() != 1 {
8021                    return Err(EvaluatorError::EvaluationError(
8022                        "abs() requires exactly 1 argument".to_string(),
8023                    ));
8024                }
8025                if evaluated_args[0].is_undefined() {
8026                    return Ok(JValue::Undefined);
8027                }
8028                match &evaluated_args[0] {
8029                    JValue::Null => Ok(JValue::Null),
8030                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
8031                    _ => Err(EvaluatorError::TypeError(
8032                        "abs() requires a number argument".to_string(),
8033                    )),
8034                }
8035            }
8036            "floor" => {
8037                if evaluated_args.len() != 1 {
8038                    return Err(EvaluatorError::EvaluationError(
8039                        "floor() requires exactly 1 argument".to_string(),
8040                    ));
8041                }
8042                if evaluated_args[0].is_undefined() {
8043                    return Ok(JValue::Undefined);
8044                }
8045                match &evaluated_args[0] {
8046                    JValue::Null => Ok(JValue::Null),
8047                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
8048                    _ => Err(EvaluatorError::TypeError(
8049                        "floor() requires a number argument".to_string(),
8050                    )),
8051                }
8052            }
8053            "ceil" => {
8054                if evaluated_args.len() != 1 {
8055                    return Err(EvaluatorError::EvaluationError(
8056                        "ceil() requires exactly 1 argument".to_string(),
8057                    ));
8058                }
8059                if evaluated_args[0].is_undefined() {
8060                    return Ok(JValue::Undefined);
8061                }
8062                match &evaluated_args[0] {
8063                    JValue::Null => Ok(JValue::Null),
8064                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
8065                    _ => Err(EvaluatorError::TypeError(
8066                        "ceil() requires a number argument".to_string(),
8067                    )),
8068                }
8069            }
8070            "round" => {
8071                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8072                    return Err(EvaluatorError::EvaluationError(
8073                        "round() requires 1 or 2 arguments".to_string(),
8074                    ));
8075                }
8076                if evaluated_args[0].is_undefined() {
8077                    return Ok(JValue::Undefined);
8078                }
8079                match &evaluated_args[0] {
8080                    JValue::Null => Ok(JValue::Null),
8081                    JValue::Number(n) => {
8082                        let precision = if evaluated_args.len() == 2 {
8083                            match &evaluated_args[1] {
8084                                JValue::Number(p) => Some(*p as i32),
8085                                _ => {
8086                                    return Err(EvaluatorError::TypeError(
8087                                        "round() precision must be a number".to_string(),
8088                                    ))
8089                                }
8090                            }
8091                        } else {
8092                            None
8093                        };
8094                        Ok(functions::numeric::round(*n, precision)?)
8095                    }
8096                    _ => Err(EvaluatorError::TypeError(
8097                        "round() requires a number argument".to_string(),
8098                    )),
8099                }
8100            }
8101            "sqrt" => {
8102                if evaluated_args.len() != 1 {
8103                    return Err(EvaluatorError::EvaluationError(
8104                        "sqrt() requires exactly 1 argument".to_string(),
8105                    ));
8106                }
8107                if evaluated_args[0].is_undefined() {
8108                    return Ok(JValue::Undefined);
8109                }
8110                match &evaluated_args[0] {
8111                    JValue::Null => Ok(JValue::Null),
8112                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
8113                    _ => Err(EvaluatorError::TypeError(
8114                        "sqrt() requires a number argument".to_string(),
8115                    )),
8116                }
8117            }
8118            "power" => {
8119                if evaluated_args.len() != 2 {
8120                    return Err(EvaluatorError::EvaluationError(
8121                        "power() requires exactly 2 arguments".to_string(),
8122                    ));
8123                }
8124                if evaluated_args[0].is_null() {
8125                    return Ok(JValue::Null);
8126                }
8127                if evaluated_args[0].is_undefined() {
8128                    return Ok(JValue::Undefined);
8129                }
8130                match (&evaluated_args[0], &evaluated_args[1]) {
8131                    (JValue::Number(base), JValue::Number(exp)) => {
8132                        Ok(functions::numeric::power(*base, *exp)?)
8133                    }
8134                    _ => Err(EvaluatorError::TypeError(
8135                        "power() requires number arguments".to_string(),
8136                    )),
8137                }
8138            }
8139            "formatNumber" => {
8140                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
8141                    return Err(EvaluatorError::EvaluationError(
8142                        "formatNumber() requires 2 or 3 arguments".to_string(),
8143                    ));
8144                }
8145                if evaluated_args[0].is_null() {
8146                    return Ok(JValue::Null);
8147                }
8148                if evaluated_args[0].is_undefined() {
8149                    return Ok(JValue::Undefined);
8150                }
8151                match (&evaluated_args[0], &evaluated_args[1]) {
8152                    (JValue::Number(num), JValue::String(picture)) => {
8153                        let options = if evaluated_args.len() == 3 {
8154                            Some(&evaluated_args[2])
8155                        } else {
8156                            None
8157                        };
8158                        Ok(functions::numeric::format_number(*num, picture, options)?)
8159                    }
8160                    _ => Err(EvaluatorError::TypeError(
8161                        "formatNumber() requires a number and a string".to_string(),
8162                    )),
8163                }
8164            }
8165            "formatBase" => {
8166                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8167                    return Err(EvaluatorError::EvaluationError(
8168                        "formatBase() requires 1 or 2 arguments".to_string(),
8169                    ));
8170                }
8171                // Handle undefined input
8172                if evaluated_args[0].is_null() {
8173                    return Ok(JValue::Null);
8174                }
8175                if evaluated_args[0].is_undefined() {
8176                    return Ok(JValue::Undefined);
8177                }
8178                match &evaluated_args[0] {
8179                    JValue::Number(num) => {
8180                        let radix = if evaluated_args.len() == 2 {
8181                            match &evaluated_args[1] {
8182                                JValue::Number(r) => Some(r.trunc() as i64),
8183                                _ => {
8184                                    return Err(EvaluatorError::TypeError(
8185                                        "formatBase() radix must be a number".to_string(),
8186                                    ))
8187                                }
8188                            }
8189                        } else {
8190                            None
8191                        };
8192                        Ok(functions::numeric::format_base(*num, radix)?)
8193                    }
8194                    _ => Err(EvaluatorError::TypeError(
8195                        "formatBase() requires a number".to_string(),
8196                    )),
8197                }
8198            }
8199            "formatInteger" => {
8200                if evaluated_args.len() != 2 {
8201                    return Err(EvaluatorError::EvaluationError(
8202                        "formatInteger() requires exactly 2 arguments".to_string(),
8203                    ));
8204                }
8205                match (&evaluated_args[0], &evaluated_args[1]) {
8206                    (JValue::Number(n), JValue::String(picture)) => {
8207                        Ok(crate::datetime::format_integer(*n, picture)?)
8208                    }
8209                    (JValue::Null, _) => Ok(JValue::Null),
8210                    (JValue::Undefined, _) => Ok(JValue::Undefined),
8211                    _ => Err(EvaluatorError::TypeError(
8212                        "formatInteger() requires a number and a string".to_string(),
8213                    )),
8214                }
8215            }
8216            "parseInteger" => {
8217                if evaluated_args.len() != 2 {
8218                    return Err(EvaluatorError::EvaluationError(
8219                        "parseInteger() requires exactly 2 arguments".to_string(),
8220                    ));
8221                }
8222                match (&evaluated_args[0], &evaluated_args[1]) {
8223                    (JValue::String(value), JValue::String(picture)) => {
8224                        Ok(crate::datetime::parse_integer(value, picture)?)
8225                    }
8226                    (JValue::Null, _) => Ok(JValue::Null),
8227                    (JValue::Undefined, _) => Ok(JValue::Undefined),
8228                    _ => Err(EvaluatorError::TypeError(
8229                        "parseInteger() requires a string and a string".to_string(),
8230                    )),
8231                }
8232            }
8233            "append" => {
8234                if evaluated_args.len() != 2 {
8235                    return Err(EvaluatorError::EvaluationError(
8236                        "append() requires exactly 2 arguments".to_string(),
8237                    ));
8238                }
8239                // Handle null/undefined arguments
8240                let first = &evaluated_args[0];
8241                let second = &evaluated_args[1];
8242
8243                // If second arg is null/undefined, return first as-is (no change)
8244                if second.is_null() || second.is_undefined() {
8245                    return Ok(first.clone());
8246                }
8247
8248                // If first arg is null/undefined, return second as-is (appending to nothing gives second)
8249                if first.is_null() || first.is_undefined() {
8250                    return Ok(second.clone());
8251                }
8252
8253                // Convert both to arrays if needed, then append
8254                let arr = match first {
8255                    JValue::Array(a) => a.to_vec(),
8256                    other => vec![other.clone()], // Wrap non-array in array
8257                };
8258
8259                // Pre-check combined size before concatenating, mirroring
8260                // jsonata-js's append() (`arg1.length + arg2.length > options.sequence`).
8261                let second_len = match second {
8262                    JValue::Array(a) => a.len(),
8263                    _ => 1,
8264                };
8265                check_sequence_length(arr.len() + second_len, &self.options)?;
8266
8267                Ok(functions::array::append(&arr, second)?)
8268            }
8269            "reverse" => {
8270                if evaluated_args.len() != 1 {
8271                    return Err(EvaluatorError::EvaluationError(
8272                        "reverse() requires exactly 1 argument".to_string(),
8273                    ));
8274                }
8275                match &evaluated_args[0] {
8276                    JValue::Null => Ok(JValue::Null),
8277                    JValue::Undefined => Ok(JValue::Undefined),
8278                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
8279                    _ => Err(EvaluatorError::TypeError(
8280                        "reverse() requires an array argument".to_string(),
8281                    )),
8282                }
8283            }
8284            "shuffle" => {
8285                if evaluated_args.len() != 1 {
8286                    return Err(EvaluatorError::EvaluationError(
8287                        "shuffle() requires exactly 1 argument".to_string(),
8288                    ));
8289                }
8290                if evaluated_args[0].is_null() {
8291                    return Ok(JValue::Null);
8292                }
8293                if evaluated_args[0].is_undefined() {
8294                    return Ok(JValue::Undefined);
8295                }
8296                match &evaluated_args[0] {
8297                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
8298                    _ => Err(EvaluatorError::TypeError(
8299                        "shuffle() requires an array argument".to_string(),
8300                    )),
8301                }
8302            }
8303
8304            "sift" => {
8305                // $sift(object, function) or $sift(function) - filter object by predicate
8306                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8307                    return Err(EvaluatorError::EvaluationError(
8308                        "sift() requires 1 or 2 arguments".to_string(),
8309                    ));
8310                }
8311
8312                // Determine which argument is the function
8313                let func_arg = if evaluated_args.len() == 1 {
8314                    &args[0]
8315                } else {
8316                    &args[1]
8317                };
8318
8319                // Detect how many parameters the callback expects
8320                let param_count = self.get_callback_param_count(func_arg);
8321
8322                // Helper function to sift a single object
8323                let sift_object = |evaluator: &mut Self,
8324                                   obj: &IndexMap<String, JValue>,
8325                                   func_node: &AstNode,
8326                                   context_data: &JValue,
8327                                   param_count: usize|
8328                 -> Result<JValue, EvaluatorError> {
8329                    // Only create the object value if callback uses 3 parameters
8330                    let obj_value = if param_count >= 3 {
8331                        Some(JValue::object(obj.clone()))
8332                    } else {
8333                        None
8334                    };
8335
8336                    let mut result = IndexMap::new();
8337                    for (key, value) in obj.iter() {
8338                        // Build argument list based on what callback expects
8339                        let call_args = match param_count {
8340                            1 => vec![value.clone()],
8341                            2 => vec![value.clone(), JValue::string(key.clone())],
8342                            _ => vec![
8343                                value.clone(),
8344                                JValue::string(key.clone()),
8345                                obj_value.as_ref().unwrap().clone(),
8346                            ],
8347                        };
8348
8349                        let pred_result =
8350                            evaluator.apply_function(func_node, &call_args, context_data)?;
8351                        if evaluator.is_truthy(&pred_result) {
8352                            result.insert(key.clone(), value.clone());
8353                        }
8354                    }
8355                    // Return undefined for empty results (will be filtered by function application)
8356                    if result.is_empty() {
8357                        Ok(JValue::Undefined)
8358                    } else {
8359                        Ok(JValue::object(result))
8360                    }
8361                };
8362
8363                // Handle partial application - if only 1 arg, use current context as object
8364                if evaluated_args.len() == 1 {
8365                    // $sift(function) - use current context data as object
8366                    let data = &normalize_lazy(data)?;
8367                    match data {
8368                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
8369                        JValue::Array(arr) => {
8370                            // Map sift over each object in the array
8371                            let mut results = Vec::new();
8372                            for item in arr.iter() {
8373                                let item = &normalize_lazy(item)?;
8374                                if let JValue::Object(o) = item {
8375                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
8376                                    // sift_object returns undefined for empty results
8377                                    if !sifted.is_undefined() {
8378                                        results.push(sifted);
8379                                    }
8380                                }
8381                            }
8382                            Ok(JValue::array(results))
8383                        }
8384                        JValue::Null => Ok(JValue::Null),
8385                        _ => Ok(JValue::Undefined),
8386                    }
8387                } else {
8388                    // $sift(object, function)
8389                    match &evaluated_args[0] {
8390                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
8391                        JValue::Null => Ok(JValue::Null),
8392                        _ => Err(EvaluatorError::TypeError(
8393                            "sift() first argument must be an object".to_string(),
8394                        )),
8395                    }
8396                }
8397            }
8398
8399            "zip" => {
8400                if evaluated_args.is_empty() {
8401                    return Err(EvaluatorError::EvaluationError(
8402                        "zip() requires at least 1 argument".to_string(),
8403                    ));
8404                }
8405
8406                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
8407                // If any argument is null/undefined, return empty array
8408                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
8409                for arg in &evaluated_args {
8410                    match arg {
8411                        JValue::Array(arr) => {
8412                            if arr.is_empty() {
8413                                // Empty array means result is empty
8414                                return Ok(JValue::array(vec![]));
8415                            }
8416                            arrays.push(arr.to_vec());
8417                        }
8418                        JValue::Null | JValue::Undefined => {
8419                            // Null/undefined means result is empty
8420                            return Ok(JValue::array(vec![]));
8421                        }
8422                        other => {
8423                            // Wrap non-array values in single-element array
8424                            arrays.push(vec![other.clone()]);
8425                        }
8426                    }
8427                }
8428
8429                if arrays.is_empty() {
8430                    return Ok(JValue::array(vec![]));
8431                }
8432
8433                // Find the length of the shortest array
8434                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
8435
8436                // Zip the arrays together
8437                let mut result = Vec::with_capacity(min_len);
8438                for i in 0..min_len {
8439                    let mut tuple = Vec::with_capacity(arrays.len());
8440                    for array in &arrays {
8441                        tuple.push(array[i].clone());
8442                    }
8443                    result.push(JValue::array(tuple));
8444                }
8445
8446                Ok(JValue::array(result))
8447            }
8448
8449            "sort" => {
8450                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8451                    return Err(EvaluatorError::EvaluationError(
8452                        "sort() requires 1 or 2 arguments".to_string(),
8453                    ));
8454                }
8455
8456                // Use pre-evaluated first argument (avoid double evaluation)
8457                let array_value = &evaluated_args[0];
8458
8459                // Handle undefined input
8460                if array_value.is_null() {
8461                    return Ok(JValue::Null);
8462                }
8463                if array_value.is_undefined() {
8464                    return Ok(JValue::Undefined);
8465                }
8466
8467                let mut arr = match array_value {
8468                    JValue::Array(arr) => arr.to_vec(),
8469                    other => vec![other.clone()],
8470                };
8471
8472                if args.len() == 2 {
8473                    // Sort using the comparator from raw args (need unevaluated lambda AST)
8474                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
8475                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
8476                    Ok(JValue::array(arr))
8477                } else {
8478                    // Default sort (no comparator)
8479                    Ok(functions::array::sort(&arr)?)
8480                }
8481            }
8482            "distinct" => {
8483                if evaluated_args.len() != 1 {
8484                    return Err(EvaluatorError::EvaluationError(
8485                        "distinct() requires exactly 1 argument".to_string(),
8486                    ));
8487                }
8488                match &evaluated_args[0] {
8489                    JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
8490                    // Non-array input, and arrays of length <= 1, pass through
8491                    // unchanged (jsonata-js functions.js:
8492                    // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
8493                    other => Ok(other.clone()),
8494                }
8495            }
8496            "exists" => {
8497                if evaluated_args.len() != 1 {
8498                    return Err(EvaluatorError::EvaluationError(
8499                        "exists() requires exactly 1 argument".to_string(),
8500                    ));
8501                }
8502                Ok(functions::array::exists(&evaluated_args[0])?)
8503            }
8504            "keys" => {
8505                if evaluated_args.len() != 1 {
8506                    return Err(EvaluatorError::EvaluationError(
8507                        "keys() requires exactly 1 argument".to_string(),
8508                    ));
8509                }
8510
8511                // Helper to unwrap single-element arrays
8512                let unwrap_single = |keys: Vec<JValue>| -> JValue {
8513                    if keys.len() == 1 {
8514                        keys.into_iter().next().unwrap()
8515                    } else {
8516                        JValue::array(keys)
8517                    }
8518                };
8519
8520                match &evaluated_args[0] {
8521                    JValue::Null => Ok(JValue::Null),
8522                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
8523                    JValue::Object(obj) => {
8524                        // Return undefined for empty objects
8525                        if obj.is_empty() {
8526                            Ok(JValue::Null)
8527                        } else {
8528                            let keys: Vec<JValue> =
8529                                obj.keys().map(|k| JValue::string(k.clone())).collect();
8530                            check_sequence_length(keys.len(), &self.options)?;
8531                            Ok(unwrap_single(keys))
8532                        }
8533                    }
8534                    JValue::Array(arr) => {
8535                        // For arrays, collect keys from all objects
8536                        let mut all_keys = Vec::new();
8537                        for item in arr.iter() {
8538                            // Skip lambda/builtin values
8539                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
8540                                continue;
8541                            }
8542                            let normalized_item = normalize_lazy(item)?;
8543                            if let JValue::Object(obj) = &normalized_item {
8544                                for key in obj.keys() {
8545                                    if !all_keys.contains(&JValue::string(key.clone())) {
8546                                        all_keys.push(JValue::string(key.clone()));
8547                                    }
8548                                }
8549                            }
8550                        }
8551                        if all_keys.is_empty() {
8552                            Ok(JValue::Null)
8553                        } else {
8554                            check_sequence_length(all_keys.len(), &self.options)?;
8555                            Ok(unwrap_single(all_keys))
8556                        }
8557                    }
8558                    // Non-object types return undefined
8559                    _ => Ok(JValue::Null),
8560                }
8561            }
8562            "lookup" => {
8563                if evaluated_args.len() != 2 {
8564                    return Err(EvaluatorError::EvaluationError(
8565                        "lookup() requires exactly 2 arguments".to_string(),
8566                    ));
8567                }
8568                if evaluated_args[0].is_null() {
8569                    return Ok(JValue::Null);
8570                }
8571                if evaluated_args[0].is_undefined() {
8572                    return Ok(JValue::Undefined);
8573                }
8574
8575                let key = match &evaluated_args[1] {
8576                    JValue::String(k) => &**k,
8577                    _ => {
8578                        return Err(EvaluatorError::TypeError(
8579                            "lookup() requires a string key".to_string(),
8580                        ))
8581                    }
8582                };
8583
8584                // Helper function to recursively lookup in values
8585                fn lookup_recursive(
8586                    val: &JValue,
8587                    key: &str,
8588                ) -> Result<Vec<JValue>, EvaluatorError> {
8589                    match val {
8590                        JValue::Array(arr) => {
8591                            let mut results = Vec::new();
8592                            for item in arr.iter() {
8593                                let nested = lookup_recursive(item, key)?;
8594                                results.extend(nested.iter().cloned());
8595                            }
8596                            Ok(results)
8597                        }
8598                        JValue::Object(obj) => {
8599                            if let Some(v) = obj.get(key) {
8600                                Ok(vec![v.clone()])
8601                            } else {
8602                                Ok(vec![])
8603                            }
8604                        }
8605                        #[cfg(feature = "python")]
8606                        JValue::LazyPyDict(lazy) => {
8607                            let v = lazy.get_field(key)?;
8608                            if v.is_undefined() {
8609                                Ok(vec![])
8610                            } else {
8611                                Ok(vec![v])
8612                            }
8613                        }
8614                        _ => Ok(vec![]),
8615                    }
8616                }
8617
8618                let results = lookup_recursive(&evaluated_args[0], key)?;
8619                if results.is_empty() {
8620                    Ok(JValue::Null)
8621                } else if results.len() == 1 {
8622                    Ok(results[0].clone())
8623                } else {
8624                    check_sequence_length(results.len(), &self.options)?;
8625                    Ok(JValue::array(results))
8626                }
8627            }
8628            "spread" => {
8629                if evaluated_args.len() != 1 {
8630                    return Err(EvaluatorError::EvaluationError(
8631                        "spread() requires exactly 1 argument".to_string(),
8632                    ));
8633                }
8634                match &evaluated_args[0] {
8635                    JValue::Null => Ok(JValue::Null),
8636                    // Not a container - pass through unchanged (e.g. so $string() still
8637                    // sees the function value and applies its own function->"" rule).
8638                    lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
8639                    JValue::Object(obj) => {
8640                        // functions::object::spread() always returns an array with one
8641                        // element per key (mirrors jsonata-js's push-per-key loop through
8642                        // this.createSequence()), so it needs the same cap as the
8643                        // array-fanout branch below and as the "keys" arm's single-object
8644                        // branch.
8645                        check_sequence_length(obj.len(), &self.options)?;
8646                        Ok(functions::object::spread(obj)?)
8647                    }
8648                    JValue::Array(arr) => {
8649                        // Spread each object in the array
8650                        let mut result = Vec::new();
8651                        for item in arr.iter() {
8652                            let normalized_item = normalize_lazy(item)?;
8653                            match &normalized_item {
8654                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
8655                                    // Skip lambdas in array
8656                                    continue;
8657                                }
8658                                JValue::Object(obj) => {
8659                                    let spread_result = functions::object::spread(obj)?;
8660                                    if let JValue::Array(spread_items) = spread_result {
8661                                        result.extend(spread_items.iter().cloned());
8662                                    } else {
8663                                        result.push(spread_result);
8664                                    }
8665                                }
8666                                // Non-objects in array are returned unchanged
8667                                other => result.push(other.clone()),
8668                            }
8669                        }
8670                        check_sequence_length(result.len(), &self.options)?;
8671                        Ok(JValue::array(result))
8672                    }
8673                    // Non-objects are returned unchanged
8674                    other => Ok(other.clone()),
8675                }
8676            }
8677            "merge" => {
8678                if evaluated_args.is_empty() {
8679                    return Err(EvaluatorError::EvaluationError(
8680                        "merge() requires at least 1 argument".to_string(),
8681                    ));
8682                }
8683                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
8684                // vs multiple object arguments: $merge(obj1, obj2)
8685                if evaluated_args.len() == 1 {
8686                    match &evaluated_args[0] {
8687                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
8688                        JValue::Null => Ok(JValue::Null),
8689                        JValue::Undefined => Ok(JValue::Undefined),
8690                        JValue::Object(_) => {
8691                            // Single object - just return it
8692                            Ok(evaluated_args[0].clone())
8693                        }
8694                        _ => Err(EvaluatorError::TypeError(
8695                            "merge() requires objects or an array of objects".to_string(),
8696                        )),
8697                    }
8698                } else {
8699                    Ok(functions::object::merge(&evaluated_args)?)
8700                }
8701            }
8702
8703            "map" => {
8704                if args.len() != 2 {
8705                    return Err(EvaluatorError::EvaluationError(
8706                        "map() requires exactly 2 arguments".to_string(),
8707                    ));
8708                }
8709
8710                // Evaluate the array argument
8711                let array = self.evaluate_internal(&args[0], data)?;
8712
8713                match array {
8714                    JValue::Array(arr) => {
8715                        // Detect how many parameters the callback expects
8716                        let param_count = self.get_callback_param_count(&args[1]);
8717
8718                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
8719                        if param_count == 1 {
8720                            if let AstNode::Lambda {
8721                                params,
8722                                body,
8723                                signature: None,
8724                                thunk: false,
8725                            } = &args[1]
8726                            {
8727                                let var_refs: Vec<&str> =
8728                                    params.iter().map(|s| s.as_str()).collect();
8729                                if let Some(compiled) =
8730                                    try_compile_expr_with_allowed_vars(body, &var_refs)
8731                                {
8732                                    let param_name = params[0].as_str();
8733                                    let mut result = Vec::with_capacity(arr.len());
8734                                    let mut vars = HashMap::new();
8735                                    for item in arr.iter() {
8736                                        vars.insert(param_name, item);
8737                                        let mapped = eval_compiled(
8738                                            &compiled,
8739                                            data,
8740                                            Some(&vars),
8741                                            &self.options,
8742                                            self.start_time,
8743                                        )?;
8744                                        if !mapped.is_undefined() {
8745                                            result.push(mapped);
8746                                        }
8747                                    }
8748                                    check_sequence_length(result.len(), &self.options)?;
8749                                    return Ok(JValue::array(result));
8750                                }
8751                            }
8752                            // Stored lambda variable fast path: $var with pre-compiled body
8753                            if let AstNode::Variable(var_name) = &args[1] {
8754                                if let Some(stored) = self.context.lookup_lambda(var_name) {
8755                                    if let Some(ref ce) = stored.compiled_body.clone() {
8756                                        let param_name = stored.params[0].clone();
8757                                        let captured_data = stored.captured_data.clone();
8758                                        let captured_env_clone = stored.captured_env.clone();
8759                                        let ce_clone = ce.clone();
8760                                        if !captured_env_clone.values().any(|v| {
8761                                            matches!(
8762                                                v,
8763                                                JValue::Lambda { .. } | JValue::Builtin { .. }
8764                                            )
8765                                        }) {
8766                                            let call_data = captured_data.as_ref().unwrap_or(data);
8767                                            let mut result = Vec::with_capacity(arr.len());
8768                                            let mut vars: HashMap<&str, &JValue> =
8769                                                captured_env_clone
8770                                                    .iter()
8771                                                    .map(|(k, v)| (k.as_str(), v))
8772                                                    .collect();
8773                                            for item in arr.iter() {
8774                                                vars.insert(param_name.as_str(), item);
8775                                                let mapped = eval_compiled(
8776                                                    &ce_clone,
8777                                                    call_data,
8778                                                    Some(&vars),
8779                                                    &self.options,
8780                                                    self.start_time,
8781                                                )?;
8782                                                if !mapped.is_undefined() {
8783                                                    result.push(mapped);
8784                                                }
8785                                            }
8786                                            check_sequence_length(result.len(), &self.options)?;
8787                                            return Ok(JValue::array(result));
8788                                        }
8789                                    }
8790                                }
8791                            }
8792                        }
8793
8794                        // Only create the array value if callback uses 3 parameters
8795                        let arr_value = if param_count >= 3 {
8796                            Some(JValue::Array(arr.clone()))
8797                        } else {
8798                            None
8799                        };
8800
8801                        let mut result = Vec::with_capacity(arr.len());
8802                        for (index, item) in arr.iter().enumerate() {
8803                            // Build argument list based on what callback expects
8804                            let call_args = match param_count {
8805                                1 => vec![item.clone()],
8806                                2 => vec![item.clone(), JValue::Number(index as f64)],
8807                                _ => vec![
8808                                    item.clone(),
8809                                    JValue::Number(index as f64),
8810                                    arr_value.as_ref().unwrap().clone(),
8811                                ],
8812                            };
8813
8814                            let mapped = self.apply_function(&args[1], &call_args, data)?;
8815                            // Filter out undefined results but keep explicit null (JSONata map semantics)
8816                            // undefined comes from missing else clause, null is explicit
8817                            if !mapped.is_undefined() {
8818                                result.push(mapped);
8819                            }
8820                        }
8821                        check_sequence_length(result.len(), &self.options)?;
8822                        Ok(JValue::array(result))
8823                    }
8824                    JValue::Null => Ok(JValue::Null),
8825                    JValue::Undefined => Ok(JValue::Undefined),
8826                    _ => Err(EvaluatorError::TypeError(
8827                        "map() first argument must be an array".to_string(),
8828                    )),
8829                }
8830            }
8831
8832            "filter" => {
8833                if args.len() != 2 {
8834                    return Err(EvaluatorError::EvaluationError(
8835                        "filter() requires exactly 2 arguments".to_string(),
8836                    ));
8837                }
8838
8839                // Evaluate the array argument
8840                let array = self.evaluate_internal(&args[0], data)?;
8841
8842                // Handle undefined input - return undefined
8843                if array.is_undefined() {
8844                    return Ok(JValue::Undefined);
8845                }
8846
8847                // Handle null input
8848                if array.is_null() {
8849                    return Ok(JValue::Undefined);
8850                }
8851
8852                // Coerce non-array values to single-element arrays
8853                // Track if input was a single value to unwrap result appropriately
8854                // Use references to avoid upfront cloning of all elements
8855                let single_holder;
8856                let (items, was_single_value): (&[JValue], bool) = match &array {
8857                    JValue::Array(arr) => (arr.as_slice(), false),
8858                    _ => {
8859                        single_holder = [array];
8860                        (&single_holder[..], true)
8861                    }
8862                };
8863
8864                // Detect how many parameters the callback expects
8865                let param_count = self.get_callback_param_count(&args[1]);
8866
8867                // CompiledExpr fast path: direct lambda with 1 param, compilable body
8868                if param_count == 1 {
8869                    if let AstNode::Lambda {
8870                        params,
8871                        body,
8872                        signature: None,
8873                        thunk: false,
8874                    } = &args[1]
8875                    {
8876                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8877                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8878                        {
8879                            let param_name = params[0].as_str();
8880                            let mut result = Vec::with_capacity(items.len() / 2);
8881                            let mut vars = HashMap::new();
8882                            for item in items.iter() {
8883                                vars.insert(param_name, item);
8884                                let pred_result = eval_compiled(
8885                                    &compiled,
8886                                    data,
8887                                    Some(&vars),
8888                                    &self.options,
8889                                    self.start_time,
8890                                )?;
8891                                if compiled_is_truthy(&pred_result) {
8892                                    result.push(item.clone());
8893                                }
8894                            }
8895                            if was_single_value {
8896                                if result.len() == 1 {
8897                                    return Ok(result.remove(0));
8898                                } else if result.is_empty() {
8899                                    return Ok(JValue::Undefined);
8900                                }
8901                            }
8902                            check_sequence_length(result.len(), &self.options)?;
8903                            return Ok(JValue::array(result));
8904                        }
8905                    }
8906                    // Stored lambda variable fast path: $var with pre-compiled body
8907                    if let AstNode::Variable(var_name) = &args[1] {
8908                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8909                            if let Some(ref ce) = stored.compiled_body.clone() {
8910                                let param_name = stored.params[0].clone();
8911                                let captured_data = stored.captured_data.clone();
8912                                let captured_env_clone = stored.captured_env.clone();
8913                                let ce_clone = ce.clone();
8914                                if !captured_env_clone.values().any(|v| {
8915                                    matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8916                                }) {
8917                                    let call_data = captured_data.as_ref().unwrap_or(data);
8918                                    let mut result = Vec::with_capacity(items.len() / 2);
8919                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
8920                                        .iter()
8921                                        .map(|(k, v)| (k.as_str(), v))
8922                                        .collect();
8923                                    for item in items.iter() {
8924                                        vars.insert(param_name.as_str(), item);
8925                                        let pred_result = eval_compiled(
8926                                            &ce_clone,
8927                                            call_data,
8928                                            Some(&vars),
8929                                            &self.options,
8930                                            self.start_time,
8931                                        )?;
8932                                        if compiled_is_truthy(&pred_result) {
8933                                            result.push(item.clone());
8934                                        }
8935                                    }
8936                                    if was_single_value {
8937                                        if result.len() == 1 {
8938                                            return Ok(result.remove(0));
8939                                        } else if result.is_empty() {
8940                                            return Ok(JValue::Undefined);
8941                                        }
8942                                    }
8943                                    check_sequence_length(result.len(), &self.options)?;
8944                                    return Ok(JValue::array(result));
8945                                }
8946                            }
8947                        }
8948                    }
8949                }
8950
8951                // Only create the array value if callback uses 3 parameters
8952                let arr_value = if param_count >= 3 {
8953                    Some(JValue::array(items.to_vec()))
8954                } else {
8955                    None
8956                };
8957
8958                let mut result = Vec::with_capacity(items.len() / 2);
8959
8960                for (index, item) in items.iter().enumerate() {
8961                    // Build argument list based on what callback expects
8962                    let call_args = match param_count {
8963                        1 => vec![item.clone()],
8964                        2 => vec![item.clone(), JValue::Number(index as f64)],
8965                        _ => vec![
8966                            item.clone(),
8967                            JValue::Number(index as f64),
8968                            arr_value.as_ref().unwrap().clone(),
8969                        ],
8970                    };
8971
8972                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8973                    if self.is_truthy(&predicate_result) {
8974                        result.push(item.clone());
8975                    }
8976                }
8977
8978                // If input was a single value, return the single matching item
8979                // (or undefined if no match)
8980                if was_single_value {
8981                    if result.len() == 1 {
8982                        return Ok(result.remove(0));
8983                    } else if result.is_empty() {
8984                        return Ok(JValue::Undefined);
8985                    }
8986                }
8987
8988                check_sequence_length(result.len(), &self.options)?;
8989                Ok(JValue::array(result))
8990            }
8991
8992            "reduce" => {
8993                if args.len() < 2 || args.len() > 3 {
8994                    return Err(EvaluatorError::EvaluationError(
8995                        "reduce() requires 2 or 3 arguments".to_string(),
8996                    ));
8997                }
8998
8999                // Check that the callback function has at least 2 parameters
9000                if let AstNode::Lambda { params, .. } = &args[1] {
9001                    if params.len() < 2 {
9002                        return Err(EvaluatorError::EvaluationError(
9003                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
9004                        ));
9005                    }
9006                } else if let AstNode::Function { name, .. } = &args[1] {
9007                    // For now, we can't validate built-in function signatures here
9008                    // But user-defined functions via lambda will be validated above
9009                    let _ = name; // avoid unused warning
9010                }
9011
9012                // Evaluate the array argument
9013                let array = self.evaluate_internal(&args[0], data)?;
9014
9015                // Convert single value to array (JSONata reduce accepts single values)
9016                // Use references to avoid upfront cloning of all elements
9017                let single_holder;
9018                let items: &[JValue] = match &array {
9019                    JValue::Array(arr) => arr.as_slice(),
9020                    JValue::Null => return Ok(JValue::Null),
9021                    _ => {
9022                        single_holder = [array];
9023                        &single_holder[..]
9024                    }
9025                };
9026
9027                if items.is_empty() {
9028                    // Return initial value if provided, otherwise null
9029                    return if args.len() == 3 {
9030                        self.evaluate_internal(&args[2], data)
9031                    } else {
9032                        Ok(JValue::Null)
9033                    };
9034                }
9035
9036                // Get initial accumulator
9037                let mut accumulator = if args.len() == 3 {
9038                    self.evaluate_internal(&args[2], data)?
9039                } else {
9040                    items[0].clone()
9041                };
9042
9043                let start_idx = if args.len() == 3 { 0 } else { 1 };
9044
9045                // Detect how many parameters the callback expects
9046                let param_count = self.get_callback_param_count(&args[1]);
9047
9048                // CompiledExpr fast path: direct lambda with 2 params, compilable body
9049                if param_count == 2 {
9050                    if let AstNode::Lambda {
9051                        params,
9052                        body,
9053                        signature: None,
9054                        thunk: false,
9055                    } = &args[1]
9056                    {
9057                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9058                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
9059                        {
9060                            let acc_name = params[0].as_str();
9061                            let item_name = params[1].as_str();
9062                            for item in items[start_idx..].iter() {
9063                                let vars: HashMap<&str, &JValue> =
9064                                    HashMap::from([(acc_name, &accumulator), (item_name, item)]);
9065                                accumulator = eval_compiled(
9066                                    &compiled,
9067                                    data,
9068                                    Some(&vars),
9069                                    &self.options,
9070                                    self.start_time,
9071                                )?;
9072                            }
9073                            return Ok(accumulator);
9074                        }
9075                    }
9076                    // Stored lambda variable fast path: $var with pre-compiled body
9077                    if let AstNode::Variable(var_name) = &args[1] {
9078                        if let Some(stored) = self.context.lookup_lambda(var_name) {
9079                            if stored.params.len() == 2 {
9080                                if let Some(ref ce) = stored.compiled_body.clone() {
9081                                    let acc_param = stored.params[0].clone();
9082                                    let item_param = stored.params[1].clone();
9083                                    let captured_data = stored.captured_data.clone();
9084                                    let captured_env_clone = stored.captured_env.clone();
9085                                    let ce_clone = ce.clone();
9086                                    if !captured_env_clone.values().any(|v| {
9087                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
9088                                    }) {
9089                                        let call_data = captured_data.as_ref().unwrap_or(data);
9090                                        for item in items[start_idx..].iter() {
9091                                            let mut vars: HashMap<&str, &JValue> =
9092                                                captured_env_clone
9093                                                    .iter()
9094                                                    .map(|(k, v)| (k.as_str(), v))
9095                                                    .collect();
9096                                            vars.insert(acc_param.as_str(), &accumulator);
9097                                            vars.insert(item_param.as_str(), item);
9098                                            // Evaluate and drop vars before assigning accumulator
9099                                            // to satisfy borrow checker (vars borrows accumulator)
9100                                            let new_acc = eval_compiled(
9101                                                &ce_clone,
9102                                                call_data,
9103                                                Some(&vars),
9104                                                &self.options,
9105                                                self.start_time,
9106                                            )?;
9107                                            drop(vars);
9108                                            accumulator = new_acc;
9109                                        }
9110                                        return Ok(accumulator);
9111                                    }
9112                                }
9113                            }
9114                        }
9115                    }
9116                }
9117
9118                // Only create the array value if callback uses 4 parameters
9119                let arr_value = if param_count >= 4 {
9120                    Some(JValue::array(items.to_vec()))
9121                } else {
9122                    None
9123                };
9124
9125                // Apply function to each element
9126                for (idx, item) in items[start_idx..].iter().enumerate() {
9127                    // For reduce, the function receives (accumulator, value, index, array)
9128                    // Callbacks may use any subset of these parameters
9129                    let actual_idx = start_idx + idx;
9130
9131                    // Build argument list based on what callback expects
9132                    let call_args = match param_count {
9133                        2 => vec![accumulator.clone(), item.clone()],
9134                        3 => vec![
9135                            accumulator.clone(),
9136                            item.clone(),
9137                            JValue::Number(actual_idx as f64),
9138                        ],
9139                        _ => vec![
9140                            accumulator.clone(),
9141                            item.clone(),
9142                            JValue::Number(actual_idx as f64),
9143                            arr_value.as_ref().unwrap().clone(),
9144                        ],
9145                    };
9146
9147                    accumulator = self.apply_function(&args[1], &call_args, data)?;
9148                }
9149
9150                Ok(accumulator)
9151            }
9152
9153            "single" => {
9154                if args.is_empty() || args.len() > 2 {
9155                    return Err(EvaluatorError::EvaluationError(
9156                        "single() requires 1 or 2 arguments".to_string(),
9157                    ));
9158                }
9159
9160                // Evaluate the array argument
9161                let array = self.evaluate_internal(&args[0], data)?;
9162
9163                // Convert to array (wrap single values)
9164                let arr = match array {
9165                    JValue::Array(arr) => arr.to_vec(),
9166                    JValue::Null => return Ok(JValue::Null),
9167                    other => vec![other],
9168                };
9169
9170                if args.len() == 1 {
9171                    // No predicate - array must have exactly 1 element
9172                    match arr.len() {
9173                        0 => Err(EvaluatorError::EvaluationError(
9174                            "single() argument is empty".to_string(),
9175                        )),
9176                        1 => Ok(arr.into_iter().next().unwrap()),
9177                        count => Err(EvaluatorError::EvaluationError(format!(
9178                            "single() argument has {} values (expected exactly 1)",
9179                            count
9180                        ))),
9181                    }
9182                } else {
9183                    // With predicate - find exactly 1 matching element
9184                    let arr_value = JValue::array(arr.clone());
9185                    let mut matches = Vec::new();
9186                    for (index, item) in arr.into_iter().enumerate() {
9187                        // Apply predicate function with (item, index, array)
9188                        let predicate_result = self.apply_function(
9189                            &args[1],
9190                            &[
9191                                item.clone(),
9192                                JValue::Number(index as f64),
9193                                arr_value.clone(),
9194                            ],
9195                            data,
9196                        )?;
9197                        if self.is_truthy(&predicate_result) {
9198                            matches.push(item);
9199                        }
9200                    }
9201
9202                    match matches.len() {
9203                        0 => Err(EvaluatorError::EvaluationError(
9204                            "single() predicate matches no values".to_string(),
9205                        )),
9206                        1 => Ok(matches.into_iter().next().unwrap()),
9207                        count => Err(EvaluatorError::EvaluationError(format!(
9208                            "single() predicate matches {} values (expected exactly 1)",
9209                            count
9210                        ))),
9211                    }
9212                }
9213            }
9214
9215            "each" => {
9216                // $each(object, function) - iterate over object, applying function to each value/key pair
9217                // Returns an array of the function results
9218                if args.is_empty() || args.len() > 2 {
9219                    return Err(EvaluatorError::EvaluationError(
9220                        "each() requires 1 or 2 arguments".to_string(),
9221                    ));
9222                }
9223
9224                // Determine which argument is the object and which is the function
9225                let (obj_value, func_arg) = if args.len() == 1 {
9226                    // Single argument: use current data as object
9227                    (data.clone(), &args[0])
9228                } else {
9229                    // Two arguments: first is object, second is function
9230                    (self.evaluate_internal(&args[0], data)?, &args[1])
9231                };
9232
9233                // Detect how many parameters the callback expects
9234                let param_count = self.get_callback_param_count(func_arg);
9235
9236                let obj_value = normalize_lazy(&obj_value)?;
9237
9238                match obj_value {
9239                    JValue::Object(obj) => {
9240                        let mut result = Vec::new();
9241                        for (key, value) in obj.iter() {
9242                            // Build argument list based on what callback expects
9243                            // The callback receives the value as the first argument and key as second
9244                            let call_args = match param_count {
9245                                1 => vec![value.clone()],
9246                                _ => vec![value.clone(), JValue::string(key.clone())],
9247                            };
9248
9249                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
9250                            // Skip undefined results (similar to map behavior)
9251                            if !fn_result.is_null() && !fn_result.is_undefined() {
9252                                result.push(fn_result);
9253                            }
9254                        }
9255                        check_sequence_length(result.len(), &self.options)?;
9256                        Ok(JValue::array(result))
9257                    }
9258                    JValue::Null => Ok(JValue::Null),
9259                    _ => Err(EvaluatorError::TypeError(
9260                        "each() first argument must be an object".to_string(),
9261                    )),
9262                }
9263            }
9264
9265            "not" => {
9266                if evaluated_args.len() != 1 {
9267                    return Err(EvaluatorError::EvaluationError(
9268                        "not() requires exactly 1 argument".to_string(),
9269                    ));
9270                }
9271                // $not(x) returns the logical negation of x
9272                // null is falsy, so $not(null) = true; undefined stays undefined
9273                if evaluated_args[0].is_undefined() {
9274                    return Ok(JValue::Undefined);
9275                }
9276                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
9277            }
9278            "boolean" => {
9279                if evaluated_args.len() != 1 {
9280                    return Err(EvaluatorError::EvaluationError(
9281                        "boolean() requires exactly 1 argument".to_string(),
9282                    ));
9283                }
9284                if evaluated_args[0].is_undefined() {
9285                    return Ok(JValue::Undefined);
9286                }
9287                Ok(functions::boolean::boolean(&evaluated_args[0])?)
9288            }
9289            "type" => {
9290                if evaluated_args.len() != 1 {
9291                    return Err(EvaluatorError::EvaluationError(
9292                        "type() requires exactly 1 argument".to_string(),
9293                    ));
9294                }
9295                // Return type string
9296                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
9297                // We use a special marker object to distinguish undefined from null
9298                match &evaluated_args[0] {
9299                    JValue::Null => Ok(JValue::string("null")),
9300                    JValue::Bool(_) => Ok(JValue::string("boolean")),
9301                    JValue::Number(_) => Ok(JValue::string("number")),
9302                    JValue::String(_) => Ok(JValue::string("string")),
9303                    JValue::Array(_) => Ok(JValue::string("array")),
9304                    JValue::Object(_) => Ok(JValue::string("object")),
9305                    JValue::Undefined => Ok(JValue::Undefined),
9306                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
9307                        Ok(JValue::string("function"))
9308                    }
9309                    JValue::Regex { .. } => Ok(JValue::string("regex")),
9310                    #[cfg(feature = "python")]
9311                    JValue::LazyPyDict(_) => Ok(JValue::string("object")),
9312                }
9313            }
9314
9315            "base64encode" => {
9316                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
9317                    return Ok(JValue::Null);
9318                }
9319                if evaluated_args.len() != 1 {
9320                    return Err(EvaluatorError::EvaluationError(
9321                        "base64encode() requires exactly 1 argument".to_string(),
9322                    ));
9323                }
9324                match &evaluated_args[0] {
9325                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
9326                    _ => Err(EvaluatorError::TypeError(
9327                        "base64encode() requires a string argument".to_string(),
9328                    )),
9329                }
9330            }
9331            "base64decode" => {
9332                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
9333                    return Ok(JValue::Null);
9334                }
9335                if evaluated_args.len() != 1 {
9336                    return Err(EvaluatorError::EvaluationError(
9337                        "base64decode() requires exactly 1 argument".to_string(),
9338                    ));
9339                }
9340                match &evaluated_args[0] {
9341                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
9342                    _ => Err(EvaluatorError::TypeError(
9343                        "base64decode() requires a string argument".to_string(),
9344                    )),
9345                }
9346            }
9347            "encodeUrlComponent" => {
9348                if evaluated_args.len() != 1 {
9349                    return Err(EvaluatorError::EvaluationError(
9350                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
9351                    ));
9352                }
9353                if evaluated_args[0].is_null() {
9354                    return Ok(JValue::Null);
9355                }
9356                if evaluated_args[0].is_undefined() {
9357                    return Ok(JValue::Undefined);
9358                }
9359                match &evaluated_args[0] {
9360                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
9361                    _ => Err(EvaluatorError::TypeError(
9362                        "encodeUrlComponent() requires a string argument".to_string(),
9363                    )),
9364                }
9365            }
9366            "decodeUrlComponent" => {
9367                if evaluated_args.len() != 1 {
9368                    return Err(EvaluatorError::EvaluationError(
9369                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
9370                    ));
9371                }
9372                if evaluated_args[0].is_null() {
9373                    return Ok(JValue::Null);
9374                }
9375                if evaluated_args[0].is_undefined() {
9376                    return Ok(JValue::Undefined);
9377                }
9378                match &evaluated_args[0] {
9379                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
9380                    _ => Err(EvaluatorError::TypeError(
9381                        "decodeUrlComponent() requires a string argument".to_string(),
9382                    )),
9383                }
9384            }
9385            "encodeUrl" => {
9386                if evaluated_args.len() != 1 {
9387                    return Err(EvaluatorError::EvaluationError(
9388                        "encodeUrl() requires exactly 1 argument".to_string(),
9389                    ));
9390                }
9391                if evaluated_args[0].is_null() {
9392                    return Ok(JValue::Null);
9393                }
9394                if evaluated_args[0].is_undefined() {
9395                    return Ok(JValue::Undefined);
9396                }
9397                match &evaluated_args[0] {
9398                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
9399                    _ => Err(EvaluatorError::TypeError(
9400                        "encodeUrl() requires a string argument".to_string(),
9401                    )),
9402                }
9403            }
9404            "decodeUrl" => {
9405                if evaluated_args.len() != 1 {
9406                    return Err(EvaluatorError::EvaluationError(
9407                        "decodeUrl() requires exactly 1 argument".to_string(),
9408                    ));
9409                }
9410                if evaluated_args[0].is_null() {
9411                    return Ok(JValue::Null);
9412                }
9413                if evaluated_args[0].is_undefined() {
9414                    return Ok(JValue::Undefined);
9415                }
9416                match &evaluated_args[0] {
9417                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
9418                    _ => Err(EvaluatorError::TypeError(
9419                        "decodeUrl() requires a string argument".to_string(),
9420                    )),
9421                }
9422            }
9423
9424            "error" => {
9425                // $error(message) - throw error with custom message
9426                if evaluated_args.is_empty() {
9427                    // No message provided
9428                    return Err(EvaluatorError::EvaluationError(
9429                        "D3137: $error() function evaluated".to_string(),
9430                    ));
9431                }
9432
9433                match &evaluated_args[0] {
9434                    JValue::String(s) => {
9435                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
9436                    }
9437                    _ => Err(EvaluatorError::TypeError(
9438                        "T0410: Argument 1 of function error does not match function signature"
9439                            .to_string(),
9440                    )),
9441                }
9442            }
9443            "assert" => {
9444                // $assert(condition, message) - throw error if condition is false
9445                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9446                    return Err(EvaluatorError::EvaluationError(
9447                        "assert() requires 1 or 2 arguments".to_string(),
9448                    ));
9449                }
9450
9451                // First argument must be a boolean
9452                let condition = match &evaluated_args[0] {
9453                    JValue::Bool(b) => *b,
9454                    _ => {
9455                        return Err(EvaluatorError::TypeError(
9456                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
9457                        ));
9458                    }
9459                };
9460
9461                if !condition {
9462                    let message = if evaluated_args.len() == 2 {
9463                        match &evaluated_args[1] {
9464                            JValue::String(s) => s.clone(),
9465                            _ => Rc::from("$assert() statement failed"),
9466                        }
9467                    } else {
9468                        Rc::from("$assert() statement failed")
9469                    };
9470                    return Err(EvaluatorError::EvaluationError(format!(
9471                        "D3141: {}",
9472                        message
9473                    )));
9474                }
9475
9476                Ok(JValue::Null)
9477            }
9478
9479            "eval" => {
9480                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
9481                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9482                    return Err(EvaluatorError::EvaluationError(
9483                        "T0410: Argument 1 of function $eval must be a string".to_string(),
9484                    ));
9485                }
9486
9487                // If the first argument is null/undefined, return undefined
9488                if evaluated_args[0].is_null() {
9489                    return Ok(JValue::Null);
9490                }
9491                if evaluated_args[0].is_undefined() {
9492                    return Ok(JValue::Undefined);
9493                }
9494
9495                // First argument must be a string expression
9496                let expr_str = match &evaluated_args[0] {
9497                    JValue::String(s) => &**s,
9498                    _ => {
9499                        return Err(EvaluatorError::EvaluationError(
9500                            "T0410: Argument 1 of function $eval must be a string".to_string(),
9501                        ));
9502                    }
9503                };
9504
9505                // Parse the expression
9506                let parsed_ast = match parser::parse(expr_str) {
9507                    Ok(ast) => ast,
9508                    Err(e) => {
9509                        // D3120 is the error code for parse errors in $eval
9510                        return Err(EvaluatorError::EvaluationError(format!(
9511                            "D3120: The expression passed to $eval cannot be parsed: {}",
9512                            e
9513                        )));
9514                    }
9515                };
9516
9517                // Determine the context to use for evaluation
9518                let eval_context = if evaluated_args.len() == 2 {
9519                    &evaluated_args[1]
9520                } else {
9521                    data
9522                };
9523
9524                // Evaluate the parsed expression
9525                match self.evaluate_internal(&parsed_ast, eval_context) {
9526                    Ok(result) => Ok(result),
9527                    Err(e) => {
9528                        // D3121 is the error code for evaluation errors in $eval
9529                        let err_msg = e.to_string();
9530                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
9531                            Err(EvaluatorError::EvaluationError(format!(
9532                                "D3121: {}",
9533                                err_msg
9534                            )))
9535                        } else {
9536                            Err(e)
9537                        }
9538                    }
9539                }
9540            }
9541
9542            "now" => {
9543                if !evaluated_args.is_empty() {
9544                    return Err(EvaluatorError::EvaluationError(
9545                        "now() takes no arguments".to_string(),
9546                    ));
9547                }
9548                Ok(crate::datetime::now())
9549            }
9550
9551            "millis" => {
9552                if !evaluated_args.is_empty() {
9553                    return Err(EvaluatorError::EvaluationError(
9554                        "millis() takes no arguments".to_string(),
9555                    ));
9556                }
9557                Ok(crate::datetime::millis())
9558            }
9559
9560            "toMillis" => {
9561                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9562                    return Err(EvaluatorError::EvaluationError(
9563                        "toMillis() requires 1 or 2 arguments".to_string(),
9564                    ));
9565                }
9566
9567                match &evaluated_args[0] {
9568                    JValue::String(s) => {
9569                        // Optional second argument is a picture string for custom parsing
9570                        if evaluated_args.len() == 2 {
9571                            match &evaluated_args[1] {
9572                                JValue::String(picture) => {
9573                                    // Use custom picture format parsing
9574                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
9575                                }
9576                                JValue::Null => Ok(JValue::Null),
9577                                JValue::Undefined => Ok(JValue::Undefined),
9578                                _ => Err(EvaluatorError::TypeError(
9579                                    "toMillis() second argument must be a string".to_string(),
9580                                )),
9581                            }
9582                        } else {
9583                            // Use ISO 8601 partial date parsing
9584                            Ok(crate::datetime::to_millis(s)?)
9585                        }
9586                    }
9587                    JValue::Null => Ok(JValue::Null),
9588                    JValue::Undefined => Ok(JValue::Undefined),
9589                    _ => Err(EvaluatorError::TypeError(
9590                        "toMillis() requires a string argument".to_string(),
9591                    )),
9592                }
9593            }
9594
9595            "fromMillis" => {
9596                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
9597                    return Err(EvaluatorError::EvaluationError(
9598                        "fromMillis() requires 1 to 3 arguments".to_string(),
9599                    ));
9600                }
9601
9602                match &evaluated_args[0] {
9603                    JValue::Number(n) => {
9604                        let millis = (if n.fract() == 0.0 {
9605                            Ok(*n as i64)
9606                        } else {
9607                            Err(())
9608                        })
9609                        .map_err(|_| {
9610                            EvaluatorError::TypeError(
9611                                "fromMillis() requires an integer".to_string(),
9612                            )
9613                        })?;
9614
9615                        let picture = match evaluated_args.get(1) {
9616                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9617                            Some(JValue::String(s)) => Some(s.to_string()),
9618                            Some(_) => {
9619                                return Err(EvaluatorError::TypeError(
9620                                    "fromMillis() second argument must be a string".to_string(),
9621                                ))
9622                            }
9623                        };
9624                        let timezone = match evaluated_args.get(2) {
9625                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9626                            Some(JValue::String(s)) => Some(s.to_string()),
9627                            Some(_) => {
9628                                return Err(EvaluatorError::TypeError(
9629                                    "fromMillis() third argument must be a string".to_string(),
9630                                ))
9631                            }
9632                        };
9633
9634                        Ok(crate::datetime::from_millis_with_picture(
9635                            millis,
9636                            picture.as_deref(),
9637                            timezone.as_deref(),
9638                        )?)
9639                    }
9640                    JValue::Null => Ok(JValue::Null),
9641                    JValue::Undefined => Ok(JValue::Undefined),
9642                    _ => Err(EvaluatorError::TypeError(
9643                        "fromMillis() requires a number argument".to_string(),
9644                    )),
9645                }
9646            }
9647
9648            _ => Err(EvaluatorError::ReferenceError(format!(
9649                "Unknown function: {}",
9650                name
9651            ))),
9652        }
9653    }
9654
9655    /// Apply a function (lambda or expression) to values
9656    ///
9657    /// This handles both:
9658    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
9659    /// 2. Simple expressions: price * 2 - evaluates with values as context
9660    fn apply_function(
9661        &mut self,
9662        func_node: &AstNode,
9663        values: &[JValue],
9664        data: &JValue,
9665    ) -> Result<JValue, EvaluatorError> {
9666        match func_node {
9667            AstNode::Lambda {
9668                params,
9669                body,
9670                signature,
9671                thunk,
9672            } => {
9673                // Direct lambda - invoke it
9674                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
9675            }
9676            AstNode::Function {
9677                name,
9678                args,
9679                is_builtin,
9680            } => {
9681                // Function call - check if it has placeholders (partial application)
9682                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
9683
9684                if has_placeholder {
9685                    // This is a partial application - evaluate it to get the lambda value
9686                    let partial_lambda =
9687                        self.create_partial_application(name, args, *is_builtin, data)?;
9688
9689                    // Now invoke the partial lambda with the provided values
9690                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
9691                        return self.invoke_stored_lambda(&stored, values, data);
9692                    }
9693                    Err(EvaluatorError::EvaluationError(
9694                        "Failed to apply partial application".to_string(),
9695                    ))
9696                } else {
9697                    // Regular function call without placeholders
9698                    // Evaluate it and apply if it returns a function
9699                    let result = self.evaluate_internal(func_node, data)?;
9700
9701                    // Check if result is a lambda value
9702                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
9703                        return self.invoke_stored_lambda(&stored, values, data);
9704                    }
9705
9706                    // Otherwise just return the result
9707                    Ok(result)
9708                }
9709            }
9710            AstNode::Variable(var_name) => {
9711                // Check if this variable holds a stored lambda
9712                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
9713                    self.invoke_stored_lambda(&stored_lambda, values, data)
9714                } else if let Some(value) = self.context.lookup(var_name).cloned() {
9715                    // Check if this variable holds a lambda value
9716                    // This handles lambdas passed as bound arguments in partial applications
9717                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
9718                        return self.invoke_stored_lambda(&stored, values, data);
9719                    }
9720                    // Regular variable value - evaluate with first value as context
9721                    if values.is_empty() {
9722                        self.evaluate_internal(func_node, data)
9723                    } else {
9724                        self.evaluate_internal(func_node, &values[0])
9725                    }
9726                } else if self.is_builtin_function(var_name) {
9727                    // This is a built-in function reference (e.g., $string, $number)
9728                    // Call it directly with the provided values (already evaluated)
9729                    self.call_builtin_with_values(var_name, values)
9730                } else {
9731                    // Unknown variable - evaluate with first value as context
9732                    if values.is_empty() {
9733                        self.evaluate_internal(func_node, data)
9734                    } else {
9735                        self.evaluate_internal(func_node, &values[0])
9736                    }
9737                }
9738            }
9739            _ => {
9740                // For non-lambda expressions, evaluate with first value as context
9741                if values.is_empty() {
9742                    self.evaluate_internal(func_node, data)
9743                } else {
9744                    self.evaluate_internal(func_node, &values[0])
9745                }
9746            }
9747        }
9748    }
9749
9750    /// Execute a transform operator on the bound $ value
9751    fn execute_transform(
9752        &mut self,
9753        location: &AstNode,
9754        update: &AstNode,
9755        delete: Option<&AstNode>,
9756        _original_data: &JValue,
9757    ) -> Result<JValue, EvaluatorError> {
9758        // Get the input value from $ binding
9759        let input = self
9760            .context
9761            .lookup("$")
9762            .ok_or_else(|| {
9763                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
9764            })?
9765            .clone();
9766
9767        // Evaluate location expression on the input to get objects to transform
9768        let located_objects = self.evaluate_internal(location, &input)?;
9769
9770        // Collect target objects into a vector for comparison
9771        let targets: Vec<JValue> = match located_objects {
9772            JValue::Array(arr) => arr.to_vec(),
9773            JValue::Object(_) => vec![located_objects],
9774            #[cfg(feature = "python")]
9775            JValue::LazyPyDict(_) => vec![located_objects],
9776            JValue::Null => Vec::new(),
9777            other => vec![other],
9778        };
9779
9780        // Validate update parameter - must be an object constructor
9781        // We need to check this before evaluation in case of errors
9782        // For now, we'll validate after evaluation in the transform helper
9783
9784        // Parse delete field names if provided
9785        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
9786            let delete_val = self.evaluate_internal(delete_node, &input)?;
9787            match delete_val {
9788                JValue::Array(arr) => arr
9789                    .iter()
9790                    .filter_map(|v| match v {
9791                        JValue::String(s) => Some(s.to_string()),
9792                        _ => None,
9793                    })
9794                    .collect(),
9795                JValue::String(s) => vec![s.to_string()],
9796                JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
9797                _ => {
9798                    // Delete parameter must be an array of strings or a string
9799                    return Err(EvaluatorError::EvaluationError(
9800                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
9801                    ));
9802                }
9803            }
9804        } else {
9805            Vec::new()
9806        };
9807
9808        // Recursive helper to apply transformation throughout the structure
9809        fn apply_transform_deep(
9810            evaluator: &mut Evaluator,
9811            value: &JValue,
9812            targets: &[JValue],
9813            update: &AstNode,
9814            delete_fields: &[String],
9815        ) -> Result<JValue, EvaluatorError> {
9816            // Check if this value is one of the targets to transform
9817            // Use JValue's PartialEq for semantic equality comparison
9818            if targets.iter().any(|t| t == value) {
9819                // Transform this object
9820                let value = &normalize_lazy(value)?;
9821                if let JValue::Object(map_rc) = value.clone() {
9822                    let mut map = (*map_rc).clone();
9823                    let update_val = evaluator.evaluate_internal(update, value)?;
9824                    // Validate that update evaluates to an object or null (undefined)
9825                    match update_val {
9826                        JValue::Object(update_map) => {
9827                            for (key, val) in update_map.iter() {
9828                                map.insert(key.clone(), val.clone());
9829                            }
9830                        }
9831                        JValue::Null | JValue::Undefined => {
9832                            // Null/undefined means no updates, just continue to deletions
9833                        }
9834                        _ => {
9835                            return Err(EvaluatorError::EvaluationError(
9836                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
9837                            ));
9838                        }
9839                    }
9840                    for field in delete_fields {
9841                        map.shift_remove(field);
9842                    }
9843                    return Ok(JValue::object(map));
9844                }
9845                return Ok(value.clone());
9846            }
9847
9848            // Otherwise, recursively process children to find and transform targets
9849            match value {
9850                JValue::Object(map) => {
9851                    let mut new_map = IndexMap::new();
9852                    for (k, v) in map.iter() {
9853                        new_map.insert(
9854                            k.clone(),
9855                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
9856                        );
9857                    }
9858                    Ok(JValue::object(new_map))
9859                }
9860                #[cfg(feature = "python")]
9861                JValue::LazyPyDict(lazy) => {
9862                    let obj = JValue::Object(lazy.to_object().map_err(EvaluatorError::from)?);
9863                    apply_transform_deep(evaluator, &obj, targets, update, delete_fields)
9864                }
9865                JValue::Array(arr) => {
9866                    let mut new_arr = Vec::new();
9867                    for item in arr.iter() {
9868                        new_arr.push(apply_transform_deep(
9869                            evaluator,
9870                            item,
9871                            targets,
9872                            update,
9873                            delete_fields,
9874                        )?);
9875                    }
9876                    Ok(JValue::array(new_arr))
9877                }
9878                _ => Ok(value.clone()),
9879            }
9880        }
9881
9882        // Apply transformation recursively starting from input
9883        apply_transform_deep(self, &input, &targets, update, &delete_fields)
9884    }
9885
9886    /// Helper to invoke a lambda with given parameters
9887    fn invoke_lambda(
9888        &mut self,
9889        params: &[String],
9890        body: &AstNode,
9891        signature: Option<&String>,
9892        values: &[JValue],
9893        data: &JValue,
9894        thunk: bool,
9895    ) -> Result<JValue, EvaluatorError> {
9896        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
9897    }
9898
9899    /// Invoke a lambda with optional captured environment (for closures)
9900    fn invoke_lambda_with_env(
9901        &mut self,
9902        params: &[String],
9903        body: &AstNode,
9904        signature: Option<&String>,
9905        values: &[JValue],
9906        data: &JValue,
9907        captured_env: Option<&HashMap<String, JValue>>,
9908        captured_data: Option<&JValue>,
9909        thunk: bool,
9910    ) -> Result<JValue, EvaluatorError> {
9911        // If this is a thunk (has tail calls), use TCO trampoline
9912        if thunk {
9913            let stored = StoredLambda {
9914                params: params.to_vec(),
9915                body: body.clone(),
9916                compiled_body: None, // Thunks use TCO, not the compiled fast path
9917                signature: signature.cloned(),
9918                captured_env: captured_env.cloned().unwrap_or_default(),
9919                captured_data: captured_data.cloned(),
9920                thunk,
9921            };
9922            return self.invoke_lambda_with_tco(&stored, values, data);
9923        }
9924
9925        // Validate signature if present, and get coerced arguments
9926        // Push a new scope for this lambda invocation
9927        self.context.push_scope();
9928
9929        // First apply captured environment (for closures)
9930        if let Some(env) = captured_env {
9931            for (name, value) in env {
9932                self.context.bind(name.clone(), value.clone());
9933            }
9934        }
9935
9936        if let Some(sig_str) = signature {
9937            // Validate and coerce arguments with signature
9938            let coerced_values = match crate::signature::Signature::parse(sig_str) {
9939                Ok(sig) => match sig.validate_and_coerce(values, data) {
9940                    Ok(coerced) => coerced,
9941                    Err(e) => {
9942                        self.context.pop_scope();
9943                        match e {
9944                            crate::signature::SignatureError::UndefinedArgument => {
9945                                return Ok(JValue::Null);
9946                            }
9947                            crate::signature::SignatureError::ArgumentTypeMismatch {
9948                                index,
9949                                expected,
9950                            } => {
9951                                return Err(EvaluatorError::TypeError(
9952                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9953                                    ));
9954                            }
9955                            crate::signature::SignatureError::ArrayTypeMismatch {
9956                                index,
9957                                expected,
9958                            } => {
9959                                return Err(EvaluatorError::TypeError(format!(
9960                                    "T0412: Argument {} of function must be an array of {}",
9961                                    index, expected
9962                                )));
9963                            }
9964                            crate::signature::SignatureError::ContextTypeMismatch {
9965                                index,
9966                                expected,
9967                            } => {
9968                                return Err(EvaluatorError::TypeError(format!(
9969                                    "T0411: Context value at argument {} does not match function signature (expected {})",
9970                                    index, expected
9971                                )));
9972                            }
9973                            _ => {
9974                                return Err(EvaluatorError::TypeError(format!(
9975                                    "Signature validation failed: {}",
9976                                    e
9977                                )));
9978                            }
9979                        }
9980                    }
9981                },
9982                Err(e) => {
9983                    self.context.pop_scope();
9984                    return Err(EvaluatorError::EvaluationError(format!(
9985                        "Invalid signature: {}",
9986                        e
9987                    )));
9988                }
9989            };
9990            // Bind coerced values to params
9991            for (i, param) in params.iter().enumerate() {
9992                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9993                self.context.bind(param.clone(), value);
9994            }
9995        } else {
9996            // No signature - bind directly from values slice (no allocation)
9997            for (i, param) in params.iter().enumerate() {
9998                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9999                self.context.bind(param.clone(), value);
10000            }
10001        }
10002
10003        // Check if this is a partial application (body is a special marker string)
10004        if let AstNode::String(body_str) = body {
10005            if body_str.starts_with("__partial_call:") {
10006                // Parse the partial call info
10007                let parts: Vec<&str> = body_str.split(':').collect();
10008                if parts.len() >= 4 {
10009                    let func_name = parts[1];
10010                    let is_builtin = parts[2] == "true";
10011                    let total_args: usize = parts[3].parse().unwrap_or(0);
10012
10013                    // Get placeholder positions from captured env
10014                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
10015                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
10016                            positions
10017                                .iter()
10018                                .filter_map(|v| v.as_f64().map(|n| n as usize))
10019                                .collect()
10020                        } else {
10021                            vec![]
10022                        }
10023                    } else {
10024                        vec![]
10025                    };
10026
10027                    // Reconstruct the full argument list
10028                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
10029
10030                    // Fill in bound arguments from captured environment
10031                    if let Some(env) = captured_env {
10032                        for (key, value) in env {
10033                            if key.starts_with("__bound_arg_") {
10034                                if let Ok(pos) = key[12..].parse::<usize>() {
10035                                    if pos < total_args {
10036                                        full_args[pos] = value.clone();
10037                                    }
10038                                }
10039                            }
10040                        }
10041                    }
10042
10043                    // Fill in placeholder positions with provided values
10044                    for (i, &pos) in placeholder_positions.iter().enumerate() {
10045                        if pos < total_args {
10046                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
10047                            full_args[pos] = value;
10048                        }
10049                    }
10050
10051                    // Pop lambda scope, then push a new scope for temp args
10052                    self.context.pop_scope();
10053                    self.context.push_scope();
10054
10055                    // Build AST nodes for the function call arguments
10056                    let mut temp_args: Vec<AstNode> = Vec::new();
10057                    for (i, value) in full_args.iter().enumerate() {
10058                        let temp_name = format!("__temp_arg_{}", i);
10059                        self.context.bind(temp_name.clone(), value.clone());
10060                        temp_args.push(AstNode::Variable(temp_name));
10061                    }
10062
10063                    // Call the original function
10064                    let result =
10065                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
10066
10067                    // Pop temp scope
10068                    self.context.pop_scope();
10069
10070                    return result;
10071                }
10072            }
10073        }
10074
10075        // Evaluate lambda body (normal case)
10076        // Use captured_data for lexical scoping if available, otherwise use call-site data
10077        let body_data = captured_data.unwrap_or(data);
10078        let result = self.evaluate_internal(body, body_data)?;
10079
10080        // Pop lambda scope, preserving any lambdas referenced by the return value
10081        // Fast path: scalar results can never contain lambda references
10082        let is_scalar = matches!(
10083            &result,
10084            JValue::Number(_)
10085                | JValue::Bool(_)
10086                | JValue::String(_)
10087                | JValue::Null
10088                | JValue::Undefined
10089        );
10090        if is_scalar {
10091            self.context.pop_scope();
10092        } else {
10093            let lambdas_to_keep = self.extract_lambda_ids(&result);
10094            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
10095        }
10096
10097        Ok(result)
10098    }
10099
10100    /// Invoke a lambda with tail call optimization using a trampoline
10101    /// This method uses an iterative loop to handle tail-recursive calls without
10102    /// growing the stack, enabling deep recursion for tail-recursive functions.
10103    fn invoke_lambda_with_tco(
10104        &mut self,
10105        stored_lambda: &StoredLambda,
10106        initial_args: &[JValue],
10107        data: &JValue,
10108    ) -> Result<JValue, EvaluatorError> {
10109        let mut current_lambda = stored_lambda.clone();
10110        let mut current_args = initial_args.to_vec();
10111        let mut current_data = data.clone();
10112
10113        // Maximum number of tail call iterations to prevent infinite loops
10114        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
10115        const MAX_TCO_ITERATIONS: usize = 100_000;
10116        let mut iterations = 0;
10117
10118        // Push a persistent scope for the TCO trampoline loop.
10119        // This scope persists across all iterations so that lambdas defined
10120        // in one iteration (like recursive $iter) remain available in subsequent ones.
10121        self.context.push_scope();
10122
10123        // Trampoline loop - keeps evaluating until we get a final value
10124        let result = loop {
10125            iterations += 1;
10126            // The hardcoded iteration cap is a backstop for when no timeout is
10127            // configured; it must not preempt a configured timeout (which is the
10128            // more specific, user-controlled guardrail). Without this gate, an
10129            // infinite tail-recursive loop with a cheap per-iteration body hits
10130            // this cap in single-digit-to-tens of milliseconds and reports the
10131            // misleading "U1001: Stack overflow" (TCO does not grow the stack;
10132            // there is no depth-500 stack here) instead of D1012, for *any*
10133            // realistic `timeout_ms` (100ms, 1s, the docs' own 5000ms default) -
10134            // defeating the purpose of the timeout guardrail for exactly the
10135            // scenario it exists to catch (see jsonata-js's own `$inf := function
10136            // (){$inf()}; $inf()` guardrails-documentation example).
10137            if self.options.timeout_ms.is_none() && iterations > MAX_TCO_ITERATIONS {
10138                self.context.pop_scope();
10139                return Err(EvaluatorError::EvaluationError(
10140                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
10141                ));
10142            }
10143            if let Err(e) = check_loop_timeout(&self.options, self.start_time) {
10144                self.context.pop_scope();
10145                return Err(e);
10146            }
10147
10148            // Evaluate the lambda body within the persistent scope
10149            let result =
10150                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
10151
10152            match result {
10153                LambdaResult::JValue(v) => break v,
10154                LambdaResult::TailCall { lambda, args, data } => {
10155                    // Continue with the tail call - no stack growth
10156                    current_lambda = *lambda;
10157                    current_args = args;
10158                    current_data = data;
10159                }
10160            }
10161        };
10162
10163        // Pop the persistent TCO scope, preserving lambdas referenced by the result
10164        let lambdas_to_keep = self.extract_lambda_ids(&result);
10165        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
10166
10167        Ok(result)
10168    }
10169
10170    /// Evaluate a lambda body, detecting tail calls for TCO
10171    /// Returns either a final value or a tail call continuation.
10172    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
10173    /// manages the persistent scope for the trampoline loop.
10174    fn invoke_lambda_body_for_tco(
10175        &mut self,
10176        lambda: &StoredLambda,
10177        values: &[JValue],
10178        data: &JValue,
10179    ) -> Result<LambdaResult, EvaluatorError> {
10180        // Validate signature if present
10181        let coerced_values = if let Some(sig_str) = &lambda.signature {
10182            match crate::signature::Signature::parse(sig_str) {
10183                Ok(sig) => match sig.validate_and_coerce(values, data) {
10184                    Ok(coerced) => coerced,
10185                    Err(e) => match e {
10186                        crate::signature::SignatureError::UndefinedArgument => {
10187                            return Ok(LambdaResult::JValue(JValue::Null));
10188                        }
10189                        crate::signature::SignatureError::ArgumentTypeMismatch {
10190                            index,
10191                            expected,
10192                        } => {
10193                            return Err(EvaluatorError::TypeError(
10194                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
10195                                    ));
10196                        }
10197                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
10198                            return Err(EvaluatorError::TypeError(format!(
10199                                "T0412: Argument {} of function must be an array of {}",
10200                                index, expected
10201                            )));
10202                        }
10203                        crate::signature::SignatureError::ContextTypeMismatch {
10204                            index,
10205                            expected,
10206                        } => {
10207                            return Err(EvaluatorError::TypeError(format!(
10208                                "T0411: Context value at argument {} does not match function signature (expected {})",
10209                                index, expected
10210                            )));
10211                        }
10212                        _ => {
10213                            return Err(EvaluatorError::TypeError(format!(
10214                                "Signature validation failed: {}",
10215                                e
10216                            )));
10217                        }
10218                    },
10219                },
10220                Err(e) => {
10221                    return Err(EvaluatorError::EvaluationError(format!(
10222                        "Invalid signature: {}",
10223                        e
10224                    )));
10225                }
10226            }
10227        } else {
10228            values.to_vec()
10229        };
10230
10231        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
10232        // Apply captured environment
10233        for (name, value) in &lambda.captured_env {
10234            self.context.bind(name.clone(), value.clone());
10235        }
10236
10237        // Bind parameters
10238        for (i, param) in lambda.params.iter().enumerate() {
10239            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
10240            self.context.bind(param.clone(), value);
10241        }
10242
10243        // Evaluate the body with tail call detection
10244        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
10245        self.evaluate_for_tco(&lambda.body, body_data)
10246    }
10247
10248    /// Evaluate an expression for TCO, detecting tail calls
10249    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
10250    fn evaluate_for_tco(
10251        &mut self,
10252        node: &AstNode,
10253        data: &JValue,
10254    ) -> Result<LambdaResult, EvaluatorError> {
10255        match node {
10256            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
10257            AstNode::Conditional {
10258                condition,
10259                then_branch,
10260                else_branch,
10261            } => {
10262                let cond_value = self.evaluate_internal(condition, data)?;
10263                let is_truthy = self.is_truthy(&cond_value);
10264
10265                if is_truthy {
10266                    self.evaluate_for_tco(then_branch, data)
10267                } else if let Some(else_expr) = else_branch {
10268                    self.evaluate_for_tco(else_expr, data)
10269                } else {
10270                    Ok(LambdaResult::JValue(JValue::Null))
10271                }
10272            }
10273
10274            // Block: evaluate all but last normally, last for TCO
10275            AstNode::Block(exprs) => {
10276                if exprs.is_empty() {
10277                    return Ok(LambdaResult::JValue(JValue::Null));
10278                }
10279
10280                // Evaluate all expressions except the last
10281                let mut result = JValue::Null;
10282                for (i, expr) in exprs.iter().enumerate() {
10283                    if i == exprs.len() - 1 {
10284                        // Last expression - evaluate for TCO
10285                        return self.evaluate_for_tco(expr, data);
10286                    } else {
10287                        result = self.evaluate_internal(expr, data)?;
10288                    }
10289                }
10290                Ok(LambdaResult::JValue(result))
10291            }
10292
10293            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
10294            AstNode::Binary {
10295                op: BinaryOp::ColonEqual,
10296                lhs,
10297                rhs,
10298            } => {
10299                // This is var := value; get the variable name
10300                let var_name = match lhs.as_ref() {
10301                    AstNode::Variable(name) => name.clone(),
10302                    _ => {
10303                        // Not a simple variable binding, evaluate normally
10304                        let result = self.evaluate_internal(node, data)?;
10305                        return Ok(LambdaResult::JValue(result));
10306                    }
10307                };
10308
10309                // Check if RHS is a lambda - store it specially
10310                if let AstNode::Lambda {
10311                    params,
10312                    body,
10313                    signature,
10314                    thunk,
10315                } = rhs.as_ref()
10316                {
10317                    let captured_env = self.capture_environment_for(body, params);
10318                    let compiled_body = if !thunk {
10319                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
10320                        try_compile_expr_with_allowed_vars(body, &var_refs)
10321                    } else {
10322                        None
10323                    };
10324                    let stored_lambda = StoredLambda {
10325                        params: params.clone(),
10326                        body: (**body).clone(),
10327                        compiled_body,
10328                        signature: signature.clone(),
10329                        captured_env,
10330                        captured_data: Some(data.clone()),
10331                        thunk: *thunk,
10332                    };
10333                    self.context.bind_lambda(var_name, stored_lambda);
10334                    let lambda_repr =
10335                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
10336                    return Ok(LambdaResult::JValue(lambda_repr));
10337                }
10338
10339                // Evaluate the RHS
10340                let value = self.evaluate_internal(rhs, data)?;
10341                self.context.bind(var_name, value.clone());
10342                Ok(LambdaResult::JValue(value))
10343            }
10344
10345            // Function call - this is where TCO happens
10346            AstNode::Function { name, args, .. } => {
10347                // Check if this is a call to a stored lambda (user function)
10348                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
10349                    if stored_lambda.thunk {
10350                        let mut evaluated_args = Vec::with_capacity(args.len());
10351                        for arg in args {
10352                            evaluated_args.push(self.evaluate_internal(arg, data)?);
10353                        }
10354                        return Ok(LambdaResult::TailCall {
10355                            lambda: Box::new(stored_lambda),
10356                            args: evaluated_args,
10357                            data: data.clone(),
10358                        });
10359                    }
10360                }
10361                // Not a thunk lambda - evaluate normally
10362                let result = self.evaluate_internal(node, data)?;
10363                Ok(LambdaResult::JValue(result))
10364            }
10365
10366            // Call node (calling a lambda value)
10367            AstNode::Call { procedure, args } => {
10368                // Evaluate the procedure to get the callable
10369                let callable = self.evaluate_internal(procedure, data)?;
10370
10371                // Check if it's a lambda with TCO
10372                if let JValue::Lambda { lambda_id, .. } = &callable {
10373                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
10374                        if stored_lambda.thunk {
10375                            let mut evaluated_args = Vec::with_capacity(args.len());
10376                            for arg in args {
10377                                evaluated_args.push(self.evaluate_internal(arg, data)?);
10378                            }
10379                            return Ok(LambdaResult::TailCall {
10380                                lambda: Box::new(stored_lambda),
10381                                args: evaluated_args,
10382                                data: data.clone(),
10383                            });
10384                        }
10385                    }
10386                }
10387                // Not a thunk - evaluate normally
10388                let result = self.evaluate_internal(node, data)?;
10389                Ok(LambdaResult::JValue(result))
10390            }
10391
10392            // Variable reference that might be a function call
10393            // This handles cases like $f($x) where $f is referenced by name
10394            AstNode::Variable(_) => {
10395                let result = self.evaluate_internal(node, data)?;
10396                Ok(LambdaResult::JValue(result))
10397            }
10398
10399            // Any other expression - evaluate normally
10400            _ => {
10401                let result = self.evaluate_internal(node, data)?;
10402                Ok(LambdaResult::JValue(result))
10403            }
10404        }
10405    }
10406
10407    /// Match with custom matcher function
10408    ///
10409    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
10410    /// The matcher function is called with the string and returns:
10411    /// { match: string, start: number, end: number, groups: [], next: function }
10412    /// The next function is called repeatedly to get subsequent matches
10413    fn match_with_custom_matcher(
10414        &mut self,
10415        str_value: &str,
10416        matcher_node: &AstNode,
10417        limit: Option<usize>,
10418        data: &JValue,
10419    ) -> Result<JValue, EvaluatorError> {
10420        let mut results = Vec::new();
10421        let mut count = 0;
10422
10423        // Call the matcher function with the string
10424        let str_val = JValue::string(str_value.to_string());
10425        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
10426
10427        // Iterate through matches following the 'next' chain
10428        while !current_match.is_undefined() && !current_match.is_null() {
10429            // Check limit
10430            if let Some(lim) = limit {
10431                if count >= lim {
10432                    break;
10433                }
10434            }
10435
10436            // Extract match information from the result object
10437            if let JValue::Object(ref match_obj) = current_match {
10438                // Validate that this is a proper match object
10439                let has_match = match_obj.contains_key("match");
10440                let has_start = match_obj.contains_key("start");
10441                let has_end = match_obj.contains_key("end");
10442                let has_groups = match_obj.contains_key("groups");
10443                let has_next = match_obj.contains_key("next");
10444
10445                if !has_match && !has_start && !has_end && !has_groups && !has_next {
10446                    // Invalid matcher result - T1010 error
10447                    return Err(EvaluatorError::EvaluationError(
10448                        "T1010: The matcher function did not return the correct object structure"
10449                            .to_string(),
10450                    ));
10451                }
10452
10453                // Build the result match object (match, index, groups)
10454                let mut result_obj = IndexMap::new();
10455
10456                if let Some(match_val) = match_obj.get("match") {
10457                    result_obj.insert("match".to_string(), match_val.clone());
10458                }
10459
10460                if let Some(start_val) = match_obj.get("start") {
10461                    result_obj.insert("index".to_string(), start_val.clone());
10462                }
10463
10464                if let Some(groups_val) = match_obj.get("groups") {
10465                    result_obj.insert("groups".to_string(), groups_val.clone());
10466                }
10467
10468                results.push(JValue::object(result_obj));
10469                count += 1;
10470
10471                // Get the next match by calling the 'next' function
10472                if let Some(next_func) = match_obj.get("next") {
10473                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
10474                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
10475                        continue;
10476                    }
10477                }
10478
10479                // No next function or couldn't call it - stop iteration
10480                break;
10481            } else {
10482                // Not a valid match object
10483                break;
10484            }
10485        }
10486
10487        // Return results
10488        if results.is_empty() {
10489            Ok(JValue::Undefined)
10490        } else {
10491            Ok(JValue::array(results))
10492        }
10493    }
10494
10495    /// Replace with lambda/function callback
10496    ///
10497    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
10498    /// The function receives a match object with: match, start, end, groups
10499    fn replace_with_lambda(
10500        &mut self,
10501        str_value: &JValue,
10502        pattern_value: &JValue,
10503        lambda_value: &JValue,
10504        limit_value: Option<&JValue>,
10505        data: &JValue,
10506    ) -> Result<JValue, EvaluatorError> {
10507        // Extract string
10508        let s = match str_value {
10509            JValue::String(s) => &**s,
10510            _ => {
10511                return Err(EvaluatorError::TypeError(
10512                    "replace() requires string arguments".to_string(),
10513                ))
10514            }
10515        };
10516
10517        // Extract regex pattern
10518        let (pattern, flags) =
10519            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
10520                EvaluatorError::TypeError(
10521                    "replace() pattern must be a regex when using lambda replacement".to_string(),
10522                )
10523            })?;
10524
10525        // Build regex
10526        let re = crate::functions::string::build_regex(&pattern, &flags)?;
10527
10528        // Parse limit
10529        let limit = if let Some(lim_val) = limit_value {
10530            match lim_val {
10531                JValue::Number(n) => {
10532                    let lim_f64 = *n;
10533                    if lim_f64 < 0.0 {
10534                        return Err(EvaluatorError::EvaluationError(format!(
10535                            "D3011: Limit must be non-negative, got {}",
10536                            lim_f64
10537                        )));
10538                    }
10539                    Some(lim_f64 as usize)
10540                }
10541                _ => {
10542                    return Err(EvaluatorError::TypeError(
10543                        "replace() limit must be a number".to_string(),
10544                    ))
10545                }
10546            }
10547        } else {
10548            None
10549        };
10550
10551        // Iterate through matches and replace using lambda
10552        let mut result = String::new();
10553        let mut last_end = 0;
10554        let mut count = 0;
10555
10556        for cap in re.captures_iter(s) {
10557            // Check limit
10558            if let Some(lim) = limit {
10559                if count >= lim {
10560                    break;
10561                }
10562            }
10563
10564            let m = cap.get(0).unwrap();
10565            let match_start = m.start();
10566            let match_end = m.end();
10567            let match_str = m.as_str();
10568
10569            // Add text before match
10570            result.push_str(&s[last_end..match_start]);
10571
10572            // Build match object
10573            let groups: Vec<JValue> = (1..cap.len())
10574                .map(|i| {
10575                    cap.get(i)
10576                        .map(|m| JValue::string(m.as_str().to_string()))
10577                        .unwrap_or(JValue::Null)
10578                })
10579                .collect();
10580
10581            let mut match_map = IndexMap::new();
10582            match_map.insert("match".to_string(), JValue::string(match_str));
10583            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
10584            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
10585            match_map.insert("groups".to_string(), JValue::array(groups));
10586            let match_obj = JValue::object(match_map);
10587
10588            // Invoke lambda with match object
10589            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
10590                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
10591            })?;
10592            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
10593            let replacement_str = match lambda_result {
10594                JValue::String(s) => s,
10595                _ => {
10596                    return Err(EvaluatorError::TypeError(format!(
10597                        "D3012: Replacement function must return a string, got {:?}",
10598                        lambda_result
10599                    )))
10600                }
10601            };
10602
10603            // Add replacement
10604            result.push_str(&replacement_str);
10605
10606            last_end = match_end;
10607            count += 1;
10608        }
10609
10610        // Add remaining text after last match
10611        result.push_str(&s[last_end..]);
10612
10613        Ok(JValue::string(result))
10614    }
10615
10616    /// Capture the current environment bindings for closure support
10617    fn capture_current_environment(&self) -> HashMap<String, JValue> {
10618        self.context.all_bindings()
10619    }
10620
10621    /// Capture only the variables referenced by a lambda body (selective capture).
10622    /// This avoids cloning the entire environment when only a few variables are needed.
10623    fn capture_environment_for(
10624        &self,
10625        body: &AstNode,
10626        params: &[String],
10627    ) -> HashMap<String, JValue> {
10628        let free_vars = Self::collect_free_variables(body, params);
10629        if free_vars.is_empty() {
10630            return HashMap::new();
10631        }
10632        let mut result = HashMap::new();
10633        for var_name in &free_vars {
10634            if let Some(value) = self.context.lookup(var_name) {
10635                result.insert(var_name.clone(), value.clone());
10636            }
10637        }
10638        result
10639    }
10640
10641    /// Collect all free variables in an AST node that are not bound by the given params.
10642    /// A "free variable" is one that is referenced but not defined within the expression.
10643    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
10644        let mut free_vars = HashSet::new();
10645        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
10646        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
10647        free_vars
10648    }
10649
10650    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
10651        match node {
10652            AstNode::Variable(name) => {
10653                if !bound.contains(name.as_str()) {
10654                    free.insert(name.clone());
10655                }
10656            }
10657            AstNode::Function { name, args, .. } => {
10658                // Function name references a variable (e.g., $f(...))
10659                if !bound.contains(name.as_str()) {
10660                    free.insert(name.clone());
10661                }
10662                for arg in args {
10663                    Self::collect_free_vars_walk(arg, bound, free);
10664                }
10665            }
10666            AstNode::Lambda { params, body, .. } => {
10667                // Inner lambda introduces new bindings
10668                let mut inner_bound = bound.clone();
10669                for p in params {
10670                    inner_bound.insert(p.as_str());
10671                }
10672                Self::collect_free_vars_walk(body, &inner_bound, free);
10673            }
10674            AstNode::Binary { op, lhs, rhs } => {
10675                Self::collect_free_vars_walk(lhs, bound, free);
10676                Self::collect_free_vars_walk(rhs, bound, free);
10677                // For ColonEqual, note: the binding is visible after this expr in blocks,
10678                // but block handling takes care of that separately
10679                let _ = op;
10680            }
10681            AstNode::Unary { operand, .. } => {
10682                Self::collect_free_vars_walk(operand, bound, free);
10683            }
10684            AstNode::Path { steps } => {
10685                for step in steps {
10686                    Self::collect_free_vars_walk(&step.node, bound, free);
10687                    for stage in &step.stages {
10688                        match stage {
10689                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
10690                            // An index stage binds a variable; it introduces no
10691                            // free variable references.
10692                            Stage::Index(_) => {}
10693                        }
10694                    }
10695                }
10696            }
10697            AstNode::Call { procedure, args } => {
10698                Self::collect_free_vars_walk(procedure, bound, free);
10699                for arg in args {
10700                    Self::collect_free_vars_walk(arg, bound, free);
10701                }
10702            }
10703            AstNode::Conditional {
10704                condition,
10705                then_branch,
10706                else_branch,
10707            } => {
10708                Self::collect_free_vars_walk(condition, bound, free);
10709                Self::collect_free_vars_walk(then_branch, bound, free);
10710                if let Some(else_expr) = else_branch {
10711                    Self::collect_free_vars_walk(else_expr, bound, free);
10712                }
10713            }
10714            AstNode::Block(exprs) => {
10715                let mut block_bound = bound.clone();
10716                for expr in exprs {
10717                    Self::collect_free_vars_walk(expr, &block_bound, free);
10718                    // Bindings introduced via := become bound for subsequent expressions
10719                    if let AstNode::Binary {
10720                        op: BinaryOp::ColonEqual,
10721                        lhs,
10722                        ..
10723                    } = expr
10724                    {
10725                        if let AstNode::Variable(var_name) = lhs.as_ref() {
10726                            block_bound.insert(var_name.as_str());
10727                        }
10728                    }
10729                }
10730            }
10731            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
10732                for expr in exprs {
10733                    Self::collect_free_vars_walk(expr, bound, free);
10734                }
10735            }
10736            AstNode::Object(pairs) => {
10737                for (key, value) in pairs {
10738                    Self::collect_free_vars_walk(key, bound, free);
10739                    Self::collect_free_vars_walk(value, bound, free);
10740                }
10741            }
10742            AstNode::ObjectTransform { input, pattern } => {
10743                Self::collect_free_vars_walk(input, bound, free);
10744                for (key, value) in pattern {
10745                    Self::collect_free_vars_walk(key, bound, free);
10746                    Self::collect_free_vars_walk(value, bound, free);
10747                }
10748            }
10749            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
10750                Self::collect_free_vars_walk(expr, bound, free);
10751            }
10752            AstNode::Sort { input, terms } => {
10753                Self::collect_free_vars_walk(input, bound, free);
10754                for (expr, _) in terms {
10755                    Self::collect_free_vars_walk(expr, bound, free);
10756                }
10757            }
10758            AstNode::Transform {
10759                location,
10760                update,
10761                delete,
10762            } => {
10763                Self::collect_free_vars_walk(location, bound, free);
10764                Self::collect_free_vars_walk(update, bound, free);
10765                if let Some(del) = delete {
10766                    Self::collect_free_vars_walk(del, bound, free);
10767                }
10768            }
10769            // Leaf nodes with no variable references
10770            AstNode::String(_)
10771            | AstNode::Name(_)
10772            | AstNode::Number(_)
10773            | AstNode::Boolean(_)
10774            | AstNode::Null
10775            | AstNode::Undefined
10776            | AstNode::Placeholder
10777            | AstNode::Regex { .. }
10778            | AstNode::Wildcard
10779            | AstNode::Descendant
10780            | AstNode::Parent(_)
10781            | AstNode::ParentVariable(_) => {}
10782        }
10783    }
10784
10785    /// Check if a name refers to a built-in function
10786    fn is_builtin_function(&self, name: &str) -> bool {
10787        matches!(
10788            name,
10789            // String functions
10790            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
10791            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
10792            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
10793            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
10794
10795            // Numeric functions
10796            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
10797            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
10798
10799            // Aggregation functions
10800            "sum" | "max" | "min" | "average" |
10801
10802            // Boolean/logic functions
10803            "boolean" | "not" | "exists" |
10804
10805            // Array functions
10806            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
10807
10808            // Object functions
10809            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
10810
10811            // Higher-order functions
10812            "map" | "filter" | "reduce" | "singletonArray" |
10813
10814            // Date/time functions
10815            "now" | "millis" | "fromMillis" | "toMillis"
10816        )
10817    }
10818
10819    /// Call a built-in function directly with pre-evaluated Values
10820    /// This is used when passing built-in functions to higher-order functions like $map
10821    fn call_builtin_with_values(
10822        &mut self,
10823        name: &str,
10824        values: &[JValue],
10825    ) -> Result<JValue, EvaluatorError> {
10826        use crate::functions;
10827
10828        if values.is_empty() {
10829            return Err(EvaluatorError::EvaluationError(format!(
10830                "{}() requires at least 1 argument",
10831                name
10832            )));
10833        }
10834
10835        // Normalize every lazy top-level argument so a conversion failure raises
10836        // TypeError here rather than being swallowed by a downstream function that
10837        // maps `LazyPyDict`/failed-conversion to a misleading result (e.g. `string()`'s
10838        // lazy arm silently stringifying to `"null"`). This is reachable from
10839        // higher-order functions passed a bare builtin reference, e.g.
10840        // `$map(items, $string)` where `items` elements are lazy (first argument),
10841        // or `$f := $append; $f(a, x)` where `x` is a lazy non-first argument.
10842        // Mirrors the blanket normalization `evaluate_function_call` applies to
10843        // ALL `evaluated_args` for inline builtin calls. Only top-level lazy
10844        // values are normalized -- arrays/objects containing lazy elements are
10845        // left as-is (pass-through preservation), same as every other dispatch
10846        // site. Guarded by `is_lazy` so the common (no lazy operand) path pays
10847        // no allocation.
10848        let normalized_values: Vec<JValue>;
10849        let values: &[JValue] = if values.iter().any(|v| v.is_lazy()) {
10850            normalized_values = values
10851                .iter()
10852                .map(|v| {
10853                    if v.is_lazy() {
10854                        normalize_lazy(v)
10855                    } else {
10856                        Ok(v.clone())
10857                    }
10858                })
10859                .collect::<Result<Vec<_>, _>>()?;
10860            &normalized_values
10861        } else {
10862            values
10863        };
10864        let arg = &values[0];
10865
10866        match name {
10867            "string" => Ok(functions::string::string(arg, None)?),
10868            "number" => Ok(functions::numeric::number(arg)?),
10869            "boolean" => Ok(functions::boolean::boolean(arg)?),
10870            "not" => {
10871                let b = functions::boolean::boolean(arg)?;
10872                match b {
10873                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
10874                    _ => Err(EvaluatorError::TypeError(
10875                        "not() requires a boolean".to_string(),
10876                    )),
10877                }
10878            }
10879            "exists" => Ok(JValue::Bool(!arg.is_null())),
10880            "abs" => match arg {
10881                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
10882                _ => Err(EvaluatorError::TypeError(
10883                    "abs() requires a number argument".to_string(),
10884                )),
10885            },
10886            "floor" => match arg {
10887                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
10888                _ => Err(EvaluatorError::TypeError(
10889                    "floor() requires a number argument".to_string(),
10890                )),
10891            },
10892            "ceil" => match arg {
10893                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
10894                _ => Err(EvaluatorError::TypeError(
10895                    "ceil() requires a number argument".to_string(),
10896                )),
10897            },
10898            "round" => match arg {
10899                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
10900                _ => Err(EvaluatorError::TypeError(
10901                    "round() requires a number argument".to_string(),
10902                )),
10903            },
10904            "sqrt" => match arg {
10905                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
10906                _ => Err(EvaluatorError::TypeError(
10907                    "sqrt() requires a number argument".to_string(),
10908                )),
10909            },
10910            "uppercase" => match arg {
10911                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
10912                JValue::Null => Ok(JValue::Null),
10913                _ => Err(EvaluatorError::TypeError(
10914                    "uppercase() requires a string argument".to_string(),
10915                )),
10916            },
10917            "lowercase" => match arg {
10918                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
10919                JValue::Null => Ok(JValue::Null),
10920                _ => Err(EvaluatorError::TypeError(
10921                    "lowercase() requires a string argument".to_string(),
10922                )),
10923            },
10924            "trim" => match arg {
10925                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
10926                JValue::Null => Ok(JValue::Null),
10927                _ => Err(EvaluatorError::TypeError(
10928                    "trim() requires a string argument".to_string(),
10929                )),
10930            },
10931            "length" => match arg {
10932                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
10933                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10934                JValue::Null => Ok(JValue::Null),
10935                _ => Err(EvaluatorError::TypeError(
10936                    "length() requires a string or array argument".to_string(),
10937                )),
10938            },
10939            "sum" => match arg {
10940                JValue::Array(arr) => {
10941                    let mut total = 0.0;
10942                    for item in arr.iter() {
10943                        match item {
10944                            JValue::Number(n) => {
10945                                total += *n;
10946                            }
10947                            _ => {
10948                                return Err(EvaluatorError::TypeError(
10949                                    "sum() requires all array elements to be numbers".to_string(),
10950                                ));
10951                            }
10952                        }
10953                    }
10954                    Ok(JValue::Number(total))
10955                }
10956                JValue::Number(n) => Ok(JValue::Number(*n)),
10957                JValue::Null => Ok(JValue::Null),
10958                _ => Err(EvaluatorError::TypeError(
10959                    "sum() requires an array of numbers".to_string(),
10960                )),
10961            },
10962            "count" => {
10963                match arg {
10964                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10965                    JValue::Null => Ok(JValue::Number(0.0)),
10966                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10967                }
10968            }
10969            "max" => match arg {
10970                JValue::Array(arr) => {
10971                    let mut max_val: Option<f64> = None;
10972                    for item in arr.iter() {
10973                        if let JValue::Number(n) = item {
10974                            let f = *n;
10975                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
10976                        }
10977                    }
10978                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10979                }
10980                JValue::Number(n) => Ok(JValue::Number(*n)),
10981                JValue::Null => Ok(JValue::Null),
10982                _ => Err(EvaluatorError::TypeError(
10983                    "max() requires an array of numbers".to_string(),
10984                )),
10985            },
10986            "min" => match arg {
10987                JValue::Array(arr) => {
10988                    let mut min_val: Option<f64> = None;
10989                    for item in arr.iter() {
10990                        if let JValue::Number(n) = item {
10991                            let f = *n;
10992                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
10993                        }
10994                    }
10995                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10996                }
10997                JValue::Number(n) => Ok(JValue::Number(*n)),
10998                JValue::Null => Ok(JValue::Null),
10999                _ => Err(EvaluatorError::TypeError(
11000                    "min() requires an array of numbers".to_string(),
11001                )),
11002            },
11003            "average" => match arg {
11004                JValue::Array(arr) => {
11005                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
11006                    if nums.is_empty() {
11007                        Ok(JValue::Null)
11008                    } else {
11009                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
11010                        Ok(JValue::Number(avg))
11011                    }
11012                }
11013                JValue::Number(n) => Ok(JValue::Number(*n)),
11014                JValue::Null => Ok(JValue::Null),
11015                _ => Err(EvaluatorError::TypeError(
11016                    "average() requires an array of numbers".to_string(),
11017                )),
11018            },
11019            "append" => {
11020                // append(array1, array2) - append second array to first
11021                if values.len() < 2 {
11022                    return Err(EvaluatorError::EvaluationError(
11023                        "append() requires 2 arguments".to_string(),
11024                    ));
11025                }
11026                let first = &values[0];
11027                let second = &values[1];
11028
11029                // Convert first to array if needed
11030                let mut result = match first {
11031                    JValue::Array(arr) => arr.to_vec(),
11032                    JValue::Null => vec![],
11033                    other => vec![other.clone()],
11034                };
11035
11036                // Append second (flatten if array)
11037                match second {
11038                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
11039                    JValue::Null => {}
11040                    other => result.push(other.clone()),
11041                }
11042
11043                check_sequence_length(result.len(), &self.options)?;
11044                Ok(JValue::array(result))
11045            }
11046            "reverse" => match arg {
11047                JValue::Array(arr) => {
11048                    let mut reversed = arr.to_vec();
11049                    reversed.reverse();
11050                    Ok(JValue::array(reversed))
11051                }
11052                JValue::Null => Ok(JValue::Null),
11053                _ => Err(EvaluatorError::TypeError(
11054                    "reverse() requires an array".to_string(),
11055                )),
11056            },
11057            "keys" => match arg {
11058                JValue::Object(obj) => {
11059                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
11060                    check_sequence_length(keys.len(), &self.options)?;
11061                    Ok(JValue::array(keys))
11062                }
11063                JValue::Null => Ok(JValue::Null),
11064                _ => Err(EvaluatorError::TypeError(
11065                    "keys() requires an object".to_string(),
11066                )),
11067            },
11068
11069            // Add more functions as needed
11070            _ => Err(EvaluatorError::ReferenceError(format!(
11071                "Built-in function {} cannot be called with values directly",
11072                name
11073            ))),
11074        }
11075    }
11076
11077    /// Collect all descendant values recursively
11078    fn collect_descendants(&self, value: &JValue) -> Result<Vec<JValue>, EvaluatorError> {
11079        let mut descendants = Vec::new();
11080
11081        match value {
11082            JValue::Null => {
11083                // Null has no descendants, return empty
11084                return Ok(descendants);
11085            }
11086            JValue::Object(obj) => {
11087                // Include the current object
11088                descendants.push(value.clone());
11089
11090                for val in obj.values() {
11091                    // Recursively collect descendants
11092                    descendants.extend(self.collect_descendants(val)?);
11093                }
11094            }
11095            #[cfg(feature = "python")]
11096            JValue::LazyPyDict(lazy) => {
11097                // Include the current (lazy) object, mirroring the Object arm
11098                descendants.push(value.clone());
11099
11100                let obj = lazy.to_object().map_err(EvaluatorError::from)?;
11101                for val in obj.values() {
11102                    descendants.extend(self.collect_descendants(val)?);
11103                }
11104            }
11105            JValue::Array(arr) => {
11106                // DO NOT include the array itself - only recurse into elements
11107                // This matches JavaScript behavior: arrays are traversed but not collected
11108                for val in arr.iter() {
11109                    // Recursively collect descendants
11110                    descendants.extend(self.collect_descendants(val)?);
11111                }
11112            }
11113            _ => {
11114                // For primitives (string, number, boolean), just include the value itself
11115                descendants.push(value.clone());
11116            }
11117        }
11118
11119        Ok(descendants)
11120    }
11121
11122    /// Evaluate a predicate (array filter or index)
11123    fn evaluate_predicate(
11124        &mut self,
11125        current: &JValue,
11126        predicate: &AstNode,
11127    ) -> Result<JValue, EvaluatorError> {
11128        // Special case: empty brackets [] (represented as Boolean(true))
11129        // This forces the value to be wrapped in an array
11130        if matches!(predicate, AstNode::Boolean(true)) {
11131            return match current {
11132                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
11133                JValue::Null => Ok(JValue::Null),
11134                other => Ok(JValue::array(vec![other.clone()])),
11135            };
11136        }
11137
11138        match current {
11139            JValue::Array(_arr) => {
11140                // Standalone predicates do simple array operations (no mapping over sub-arrays)
11141
11142                // First, try to evaluate predicate as a simple number (array index)
11143                if let AstNode::Number(n) = predicate {
11144                    // Direct array indexing
11145                    return self.array_index(current, &JValue::Number(*n));
11146                }
11147
11148                // Fast path: if predicate is definitely a filter expression (comparison/logical),
11149                // skip speculative numeric evaluation and go directly to filter logic
11150                if Self::is_filter_predicate(predicate) {
11151                    // Try CompiledExpr fast path
11152                    if let Some(compiled) = try_compile_expr(predicate) {
11153                        let shape = _arr.first().and_then(build_shape_cache);
11154                        let mut filtered = Vec::with_capacity(_arr.len());
11155                        for item in _arr.iter() {
11156                            let result = if let Some(ref s) = shape {
11157                                eval_compiled_shaped(
11158                                    &compiled,
11159                                    item,
11160                                    None,
11161                                    s,
11162                                    &self.options,
11163                                    self.start_time,
11164                                )?
11165                            } else {
11166                                eval_compiled(
11167                                    &compiled,
11168                                    item,
11169                                    None,
11170                                    &self.options,
11171                                    self.start_time,
11172                                )?
11173                            };
11174                            if compiled_is_truthy(&result) {
11175                                filtered.push(item.clone());
11176                            }
11177                        }
11178                        return Ok(JValue::array(filtered));
11179                    }
11180                    // Fallback: full AST evaluation per element
11181                    let mut filtered = Vec::new();
11182                    for item in _arr.iter() {
11183                        let item_result = self.evaluate_internal(predicate, item)?;
11184                        if self.is_truthy(&item_result) {
11185                            filtered.push(item.clone());
11186                        }
11187                    }
11188                    return Ok(JValue::array(filtered));
11189                }
11190
11191                // Try to evaluate the predicate to see if it's a numeric index
11192                // If evaluation succeeds and yields a number, use it as an index
11193                // If evaluation fails (e.g., comparison error), treat as filter
11194                match self.evaluate_internal(predicate, current) {
11195                    Ok(JValue::Number(_)) => {
11196                        // It's a numeric index
11197                        let pred_result = self.evaluate_internal(predicate, current)?;
11198                        return self.array_index(current, &pred_result);
11199                    }
11200                    Ok(JValue::Array(indices)) => {
11201                        // Multiple array selectors [[indices]]
11202                        // Check if array contains any non-numeric values
11203                        let has_non_numeric =
11204                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
11205
11206                        if has_non_numeric {
11207                            // If array contains non-numeric values, return entire array
11208                            return Ok(current.clone());
11209                        }
11210
11211                        // Collect numeric indices, handling negative indices
11212                        let arr_len = _arr.len() as i64;
11213                        let mut resolved_indices: Vec<i64> = indices
11214                            .iter()
11215                            .filter_map(|v| {
11216                                if let JValue::Number(n) = v {
11217                                    let idx = *n as i64;
11218                                    // Resolve negative indices
11219                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
11220                                    // Only include valid indices
11221                                    if actual_idx >= 0 && actual_idx < arr_len {
11222                                        Some(actual_idx)
11223                                    } else {
11224                                        None
11225                                    }
11226                                } else {
11227                                    None
11228                                }
11229                            })
11230                            .collect();
11231
11232                        // Sort and deduplicate indices
11233                        resolved_indices.sort();
11234                        resolved_indices.dedup();
11235
11236                        // Select elements at each sorted index
11237                        let result: Vec<JValue> = resolved_indices
11238                            .iter()
11239                            .map(|&idx| _arr[idx as usize].clone())
11240                            .collect();
11241
11242                        return Ok(JValue::array(result));
11243                    }
11244                    Ok(_) => {
11245                        // Evaluated successfully but not a number - might be a filter
11246                        // Fall through to filter logic
11247                    }
11248                    Err(_) => {
11249                        // Evaluation failed - it's likely a filter expression
11250                        // Fall through to filter logic
11251                    }
11252                }
11253
11254                // Try CompiledExpr fast path for filter expressions
11255                if let Some(compiled) = try_compile_expr(predicate) {
11256                    let shape = _arr.first().and_then(build_shape_cache);
11257                    let mut filtered = Vec::with_capacity(_arr.len());
11258                    for item in _arr.iter() {
11259                        let result = if let Some(ref s) = shape {
11260                            eval_compiled_shaped(
11261                                &compiled,
11262                                item,
11263                                None,
11264                                s,
11265                                &self.options,
11266                                self.start_time,
11267                            )?
11268                        } else {
11269                            eval_compiled(&compiled, item, None, &self.options, self.start_time)?
11270                        };
11271                        if compiled_is_truthy(&result) {
11272                            filtered.push(item.clone());
11273                        }
11274                    }
11275                    return Ok(JValue::array(filtered));
11276                }
11277
11278                // It's a filter expression - evaluate the predicate for each array element
11279                let mut filtered = Vec::new();
11280                for item in _arr.iter() {
11281                    let item_result = self.evaluate_internal(predicate, item)?;
11282
11283                    // If result is truthy, include this item
11284                    if self.is_truthy(&item_result) {
11285                        filtered.push(item.clone());
11286                    }
11287                }
11288
11289                Ok(JValue::array(filtered))
11290            }
11291            JValue::Object(obj) => {
11292                // For objects, predicate can be either:
11293                // 1. A string - property access (computed property name)
11294                // 2. A boolean expression - filter (return object if truthy)
11295                let pred_result = self.evaluate_internal(predicate, current)?;
11296
11297                // If it's a string, use it as a key for property access
11298                if let JValue::String(key) = &pred_result {
11299                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
11300                }
11301
11302                // Otherwise, treat as a filter expression
11303                // If the predicate is truthy, return the object; otherwise return undefined
11304                if self.is_truthy(&pred_result) {
11305                    Ok(current.clone())
11306                } else {
11307                    Ok(JValue::Undefined)
11308                }
11309            }
11310            _ => {
11311                // For primitive values (string, number, boolean):
11312                // In JSONata, scalars are treated as single-element arrays when indexed.
11313                // So value[0] returns value, value[1] returns undefined.
11314
11315                // First check if predicate is a numeric literal
11316                if let AstNode::Number(n) = predicate {
11317                    // For scalars, index 0 or -1 returns the value, others return undefined
11318                    let idx = n.floor() as i64;
11319                    if idx == 0 || idx == -1 {
11320                        return Ok(current.clone());
11321                    } else {
11322                        return Ok(JValue::Undefined);
11323                    }
11324                }
11325
11326                // Try to evaluate the predicate to see if it's a numeric index
11327                let pred_result = self.evaluate_internal(predicate, current)?;
11328
11329                if let JValue::Number(n) = &pred_result {
11330                    // It's a numeric index - treat scalar as single-element array
11331                    let idx = n.floor() as i64;
11332                    if idx == 0 || idx == -1 {
11333                        return Ok(current.clone());
11334                    } else {
11335                        return Ok(JValue::Undefined);
11336                    }
11337                }
11338
11339                // For non-numeric predicates, treat as a filter:
11340                // value[true] returns value, value[false] returns undefined
11341                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
11342                if self.is_truthy(&pred_result) {
11343                    Ok(current.clone())
11344                } else {
11345                    // Return undefined (not null) so $map can filter it out
11346                    Ok(JValue::Undefined)
11347                }
11348            }
11349        }
11350    }
11351
11352    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
11353    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
11354    fn evaluate_sort_term(
11355        &mut self,
11356        term_expr: &AstNode,
11357        element: &JValue,
11358    ) -> Result<JValue, EvaluatorError> {
11359        // For tuples (from index binding), extract the actual value from @ field
11360        let actual_element = if let JValue::Object(obj) = element {
11361            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
11362                obj.get("@").cloned().unwrap_or(JValue::Null)
11363            } else {
11364                element.clone()
11365            }
11366        } else {
11367            element.clone()
11368        };
11369
11370        // For simple field access (Path with single Name step), check if field exists
11371        if let AstNode::Path { steps } = term_expr {
11372            if steps.len() == 1 && steps[0].stages.is_empty() {
11373                if let AstNode::Name(field_name) = &steps[0].node {
11374                    // Check if the field exists in the element
11375                    match &actual_element {
11376                        JValue::Object(obj) => {
11377                            return match obj.get(field_name) {
11378                                Some(val) => Ok(val.clone()),  // Field exists (may be null)
11379                                None => Ok(JValue::Undefined), // Field is missing
11380                            };
11381                        }
11382                        #[cfg(feature = "python")]
11383                        JValue::LazyPyDict(lazy) => {
11384                            return Ok(lazy.get_field(field_name)?);
11385                        }
11386                        _ => return Ok(JValue::Undefined),
11387                    }
11388                }
11389            }
11390        }
11391
11392        // For complex expressions, evaluate against the tuple's `@` value (the
11393        // real element), not the wrapper. The tuple's carried focus/index/ancestor
11394        // bindings are reachable via context (bound by evaluate_sort), so a term
11395        // like `$`, `%.Price`, or `$pos` still resolves correctly.
11396        let result = self.evaluate_internal(term_expr, &actual_element)?;
11397
11398        // If the result is null from a complex expression, we can't easily tell if it's
11399        // "missing field" or "explicit null". For now, treat null results as undefined
11400        // to maintain compatibility with existing tests.
11401        // TODO: For full JS compatibility, would need deeper analysis of the expression
11402        if result.is_null() {
11403            return Ok(JValue::Undefined);
11404        }
11405
11406        Ok(result)
11407    }
11408
11409    /// Evaluate sort operator
11410    fn evaluate_sort(
11411        &mut self,
11412        data: &JValue,
11413        terms: &[(AstNode, bool)],
11414    ) -> Result<JValue, EvaluatorError> {
11415        // If data is null, return null
11416        if data.is_null() {
11417            return Ok(JValue::Null);
11418        }
11419
11420        // If data is not an array, return it as-is (can't sort a single value)
11421        let array = match data {
11422            JValue::Array(arr) => arr.clone(),
11423            other => return Ok(other.clone()),
11424        };
11425
11426        // If empty array, return as-is
11427        if array.is_empty() {
11428            return Ok(JValue::Array(array));
11429        }
11430
11431        // Evaluate sort keys for each element
11432        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
11433
11434        for (idx, element) in array.iter().enumerate() {
11435            let mut sort_keys = Vec::new();
11436
11437            // When sorting a tuple stream (the input path had a `%`/`@`/`#`
11438            // step, so each element is a `{@, !label, $var, __tuple__}`
11439            // wrapper), bind its carried ancestor/focus/index keys into scope
11440            // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
11441            // create_tuple_stream's per-tuple frame binding. Sort terms attach
11442            // to a synthetic step after the last input step, so `%` refers to
11443            // the last input step's ancestry, carried under `!label` here.
11444            // Saves/restores rather than blindly unbinding, so a tuple key
11445            // that collides with a live outer `:=` binding doesn't get
11446            // deleted once this row's sort terms are evaluated.
11447            let tuple_bindings = match element {
11448                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11449                    Some(self.bind_tuple_keys(obj))
11450                }
11451                _ => None,
11452            };
11453
11454            // When sorting a tuple stream, `$` and the term's data context are the
11455            // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
11456            // otherwise a term like `^($)` would try to order by the wrapper
11457            // object and raise T2008. The carried focus/index/ancestor keys stay
11458            // reachable via the context bindings established just above.
11459            let term_data = match element {
11460                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11461                    obj.get("@").cloned().unwrap_or(JValue::Null)
11462                }
11463                other => other.clone(),
11464            };
11465
11466            // Evaluate each sort term with $ bound to the element
11467            for (term_expr, _ascending) in terms {
11468                // Save current $ binding
11469                let saved_dollar = self.context.lookup("$").cloned();
11470
11471                // Bind $ to current element
11472                self.context.bind("$".to_string(), term_data.clone());
11473
11474                // Evaluate the sort expression, distinguishing missing fields from explicit null
11475                let sort_value = self.evaluate_sort_term(term_expr, element)?;
11476
11477                // Restore $ binding
11478                if let Some(val) = saved_dollar {
11479                    self.context.bind("$".to_string(), val);
11480                } else {
11481                    self.context.unbind("$");
11482                }
11483
11484                sort_keys.push(sort_value);
11485            }
11486
11487            if let Some(tuple_bindings) = tuple_bindings {
11488                tuple_bindings.restore(self);
11489            }
11490
11491            indexed_array.push((idx, sort_keys));
11492        }
11493
11494        // Validate that all sort keys are comparable (same type, or undefined)
11495        // Undefined values (missing fields) are allowed and sort to the end
11496        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
11497        for term_idx in 0..terms.len() {
11498            let mut first_valid_type: Option<&str> = None;
11499
11500            for (_idx, sort_keys) in &indexed_array {
11501                let sort_value = &sort_keys[term_idx];
11502
11503                // Skip undefined markers (missing fields) - these are allowed and sort to end
11504                if sort_value.is_undefined() {
11505                    continue;
11506                }
11507
11508                // Get the type name for this value
11509                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
11510                let value_type = match sort_value {
11511                    JValue::Number(_) => "number",
11512                    JValue::String(_) => "string",
11513                    JValue::Bool(_) => "boolean",
11514                    JValue::Array(_) => "array",
11515                    JValue::Object(_) => "object", // This catches non-undefined objects
11516                    JValue::Null => "null",        // Explicit null from data
11517                    #[cfg(feature = "python")]
11518                    JValue::LazyPyDict(_) => "object",
11519                    _ => "unknown",
11520                };
11521
11522                // Check that sort keys are only numbers or strings
11523                // Null, boolean, array, and object types are not valid for sorting
11524                if value_type != "number" && value_type != "string" {
11525                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
11526                }
11527
11528                // Check if this matches the first valid type we saw
11529                if let Some(first_type) = first_valid_type {
11530                    if first_type != value_type {
11531                        return Err(EvaluatorError::TypeError(format!(
11532                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
11533                            first_type, value_type
11534                        )));
11535                    }
11536                } else {
11537                    first_valid_type = Some(value_type);
11538                }
11539            }
11540        }
11541
11542        // Sort the indexed array
11543        indexed_array.sort_by(|a, b| {
11544            // Compare sort keys in order
11545            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
11546                let left = &a.1[i];
11547                let right = &b.1[i];
11548
11549                let cmp = self.compare_values(left, right);
11550
11551                if cmp != std::cmp::Ordering::Equal {
11552                    return if *ascending { cmp } else { cmp.reverse() };
11553                }
11554            }
11555
11556            // If all keys are equal, maintain original order (stable sort)
11557            a.0.cmp(&b.0)
11558        });
11559
11560        // Extract sorted elements
11561        let sorted: Vec<JValue> = indexed_array
11562            .iter()
11563            .map(|(idx, _)| array[*idx].clone())
11564            .collect();
11565
11566        Ok(JValue::array(sorted))
11567    }
11568
11569    /// Compare two values for sorting (JSONata semantics)
11570    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
11571        // Handle undefined markers first - they sort to the end
11572        let left_undef = left.is_undefined();
11573        let right_undef = right.is_undefined();
11574
11575        if left_undef && right_undef {
11576            return Ordering::Equal;
11577        }
11578        if left_undef {
11579            return Ordering::Greater; // Undefined sorts last
11580        }
11581        if right_undef {
11582            return Ordering::Less;
11583        }
11584
11585        match (left, right) {
11586            // Nulls also sort last (explicit null in data)
11587            (JValue::Null, JValue::Null) => Ordering::Equal,
11588            (JValue::Null, _) => Ordering::Greater,
11589            (_, JValue::Null) => Ordering::Less,
11590
11591            // Numbers
11592            (JValue::Number(a), JValue::Number(b)) => {
11593                let a_f64 = *a;
11594                let b_f64 = *b;
11595                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
11596            }
11597
11598            // Strings
11599            (JValue::String(a), JValue::String(b)) => a.cmp(b),
11600
11601            // Booleans
11602            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
11603
11604            // Arrays (lexicographic comparison)
11605            (JValue::Array(a), JValue::Array(b)) => {
11606                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
11607                    let cmp = self.compare_values(a_elem, b_elem);
11608                    if cmp != Ordering::Equal {
11609                        return cmp;
11610                    }
11611                }
11612                a.len().cmp(&b.len())
11613            }
11614
11615            // Different types: use type ordering
11616            // null < bool < number < string < array < object
11617            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
11618            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
11619            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
11620            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
11621            #[cfg(feature = "python")]
11622            (JValue::Bool(_), JValue::LazyPyDict(_)) => Ordering::Less,
11623
11624            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
11625            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
11626            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
11627            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
11628            #[cfg(feature = "python")]
11629            (JValue::Number(_), JValue::LazyPyDict(_)) => Ordering::Less,
11630
11631            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
11632            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
11633            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
11634            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
11635            #[cfg(feature = "python")]
11636            (JValue::String(_), JValue::LazyPyDict(_)) => Ordering::Less,
11637
11638            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
11639            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
11640            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
11641            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
11642            #[cfg(feature = "python")]
11643            (JValue::Array(_), JValue::LazyPyDict(_)) => Ordering::Less,
11644
11645            (JValue::Object(_), _) => Ordering::Greater,
11646            #[cfg(feature = "python")]
11647            (JValue::LazyPyDict(_), _) => Ordering::Greater,
11648            _ => Ordering::Equal,
11649        }
11650    }
11651
11652    /// Check if a value is truthy (JSONata semantics).
11653    fn is_truthy(&self, value: &JValue) -> bool {
11654        match value {
11655            JValue::Null | JValue::Undefined => false,
11656            JValue::Bool(b) => *b,
11657            JValue::Number(n) => *n != 0.0,
11658            JValue::String(s) => !s.is_empty(),
11659            JValue::Array(arr) => !arr.is_empty(),
11660            JValue::Object(obj) => !obj.is_empty(),
11661            #[cfg(feature = "python")]
11662            JValue::LazyPyDict(lazy) => !lazy.is_empty(),
11663            _ => false,
11664        }
11665    }
11666
11667    /// Check if a value is truthy for the default operator (?:)
11668    /// This has special semantics:
11669    /// - Lambda/function objects are not values, so they're falsy
11670    /// - Arrays containing only falsy elements are falsy
11671    /// - Otherwise, use standard truthiness
11672    fn is_truthy_for_default(&self, value: &JValue) -> bool {
11673        match value {
11674            // Lambda/function values are not data values, so they're falsy
11675            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
11676            // Arrays need special handling - check if all elements are falsy
11677            JValue::Array(arr) => {
11678                if arr.is_empty() {
11679                    return false;
11680                }
11681                // Array is truthy only if it contains at least one truthy element
11682                arr.iter().any(|elem| self.is_truthy(elem))
11683            }
11684            // For all other types, use standard truthiness
11685            _ => self.is_truthy(value),
11686        }
11687    }
11688
11689    /// Unwrap singleton arrays to scalar values
11690    /// This is used when no explicit array-keeping operation (like []) was used
11691    fn unwrap_singleton(&self, value: JValue) -> JValue {
11692        match value {
11693            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
11694            _ => value,
11695        }
11696    }
11697
11698    /// Extract lambda IDs from a value (used for closure preservation)
11699    /// Finds any lambda_id references in the value so they can be preserved
11700    /// when exiting a block scope
11701    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
11702        // Fast path: scalars can never contain lambda references
11703        match value {
11704            JValue::Number(_)
11705            | JValue::Bool(_)
11706            | JValue::String(_)
11707            | JValue::Null
11708            | JValue::Undefined
11709            | JValue::Regex { .. }
11710            | JValue::Builtin { .. } => return Vec::new(),
11711            _ => {}
11712        }
11713        let mut ids = Vec::new();
11714        self.collect_lambda_ids(value, &mut ids);
11715        ids
11716    }
11717
11718    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
11719        match value {
11720            JValue::Lambda { lambda_id, .. } => {
11721                let id_str = lambda_id.to_string();
11722                if !ids.contains(&id_str) {
11723                    ids.push(id_str);
11724                    // Transitively follow the stored lambda's captured_env
11725                    // to find all referenced lambdas. This is critical for
11726                    // closures like the Y-combinator where returned lambdas
11727                    // capture other lambdas in their environment.
11728                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
11729                        let env_values: Vec<JValue> =
11730                            stored.captured_env.values().cloned().collect();
11731                        for env_value in &env_values {
11732                            self.collect_lambda_ids(env_value, ids);
11733                        }
11734                    }
11735                }
11736            }
11737            JValue::Object(map) => {
11738                // Recurse into object values
11739                for v in map.values() {
11740                    self.collect_lambda_ids(v, ids);
11741                }
11742            }
11743            JValue::Array(arr) => {
11744                // Recurse into array elements
11745                for v in arr.iter() {
11746                    self.collect_lambda_ids(v, ids);
11747                }
11748            }
11749            _ => {}
11750        }
11751    }
11752
11753    /// Equality comparison (JSONata semantics)
11754    fn equals(&self, left: &JValue, right: &JValue) -> bool {
11755        crate::functions::array::values_equal(left, right)
11756    }
11757
11758    /// Addition
11759    fn add(
11760        &self,
11761        left: &JValue,
11762        right: &JValue,
11763        left_is_explicit_null: bool,
11764        right_is_explicit_null: bool,
11765    ) -> Result<JValue, EvaluatorError> {
11766        match (left, right) {
11767            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
11768            // Explicit null literal with number -> T2002 error
11769            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
11770                Err(EvaluatorError::TypeError(
11771                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11772                ))
11773            }
11774            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
11775                Err(EvaluatorError::TypeError(
11776                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
11777                ))
11778            }
11779            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
11780                Err(EvaluatorError::TypeError(
11781                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
11782                ))
11783            }
11784            // Undefined variable (null/undefined) with number -> undefined result
11785            (JValue::Null | JValue::Undefined, JValue::Number(_))
11786            | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
11787            // Boolean with anything (including undefined) -> T2001 error
11788            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
11789                "T2001: The left side of the '+' operator must evaluate to a number or a string"
11790                    .to_string(),
11791            )),
11792            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
11793                "T2001: The right side of the '+' operator must evaluate to a number or a string"
11794                    .to_string(),
11795            )),
11796            // Undefined with undefined -> undefined
11797            (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
11798                Ok(JValue::Null)
11799            }
11800            _ => Err(EvaluatorError::TypeError(format!(
11801                "Cannot add {:?} and {:?}",
11802                left, right
11803            ))),
11804        }
11805    }
11806
11807    /// Subtraction
11808    fn subtract(
11809        &self,
11810        left: &JValue,
11811        right: &JValue,
11812        left_is_explicit_null: bool,
11813        right_is_explicit_null: bool,
11814    ) -> Result<JValue, EvaluatorError> {
11815        match (left, right) {
11816            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
11817            // Explicit null literal -> error
11818            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11819                "T2002: The left side of the - operator must evaluate to a number".to_string(),
11820            )),
11821            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11822                "T2002: The right side of the - operator must evaluate to a number".to_string(),
11823            )),
11824            // Undefined variables -> undefined result
11825            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11826                Ok(JValue::Null)
11827            }
11828            _ => Err(EvaluatorError::TypeError(format!(
11829                "Cannot subtract {:?} and {:?}",
11830                left, right
11831            ))),
11832        }
11833    }
11834
11835    /// Multiplication
11836    fn multiply(
11837        &self,
11838        left: &JValue,
11839        right: &JValue,
11840        left_is_explicit_null: bool,
11841        right_is_explicit_null: bool,
11842    ) -> Result<JValue, EvaluatorError> {
11843        match (left, right) {
11844            (JValue::Number(a), JValue::Number(b)) => {
11845                let result = *a * *b;
11846                // Check for overflow to Infinity
11847                if result.is_infinite() {
11848                    return Err(EvaluatorError::EvaluationError(
11849                        "D1001: Number out of range".to_string(),
11850                    ));
11851                }
11852                Ok(JValue::Number(result))
11853            }
11854            // Explicit null literal -> error
11855            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11856                "T2002: The left side of the * operator must evaluate to a number".to_string(),
11857            )),
11858            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11859                "T2002: The right side of the * operator must evaluate to a number".to_string(),
11860            )),
11861            // Undefined variables -> undefined result
11862            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11863                Ok(JValue::Null)
11864            }
11865            _ => Err(EvaluatorError::TypeError(format!(
11866                "Cannot multiply {:?} and {:?}",
11867                left, right
11868            ))),
11869        }
11870    }
11871
11872    /// Division
11873    fn divide(
11874        &self,
11875        left: &JValue,
11876        right: &JValue,
11877        left_is_explicit_null: bool,
11878        right_is_explicit_null: bool,
11879    ) -> Result<JValue, EvaluatorError> {
11880        match (left, right) {
11881            (JValue::Number(a), JValue::Number(b)) => {
11882                let denominator = *b;
11883                if denominator == 0.0 {
11884                    return Err(EvaluatorError::EvaluationError(
11885                        "Division by zero".to_string(),
11886                    ));
11887                }
11888                Ok(JValue::Number(*a / denominator))
11889            }
11890            // Explicit null literal -> error
11891            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11892                "T2002: The left side of the / operator must evaluate to a number".to_string(),
11893            )),
11894            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11895                "T2002: The right side of the / operator must evaluate to a number".to_string(),
11896            )),
11897            // Undefined variables -> undefined result
11898            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11899                Ok(JValue::Null)
11900            }
11901            _ => Err(EvaluatorError::TypeError(format!(
11902                "Cannot divide {:?} and {:?}",
11903                left, right
11904            ))),
11905        }
11906    }
11907
11908    /// Modulo
11909    fn modulo(
11910        &self,
11911        left: &JValue,
11912        right: &JValue,
11913        left_is_explicit_null: bool,
11914        right_is_explicit_null: bool,
11915    ) -> Result<JValue, EvaluatorError> {
11916        match (left, right) {
11917            (JValue::Number(a), JValue::Number(b)) => {
11918                let denominator = *b;
11919                if denominator == 0.0 {
11920                    return Err(EvaluatorError::EvaluationError(
11921                        "Division by zero".to_string(),
11922                    ));
11923                }
11924                Ok(JValue::Number(*a % denominator))
11925            }
11926            // Explicit null literal -> error
11927            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11928                "T2002: The left side of the % operator must evaluate to a number".to_string(),
11929            )),
11930            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11931                "T2002: The right side of the % operator must evaluate to a number".to_string(),
11932            )),
11933            // Undefined variables -> undefined result
11934            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11935                Ok(JValue::Null)
11936            }
11937            _ => Err(EvaluatorError::TypeError(format!(
11938                "Cannot compute modulo of {:?} and {:?}",
11939                left, right
11940            ))),
11941        }
11942    }
11943
11944    /// Get human-readable type name for error messages
11945    fn type_name(value: &JValue) -> &'static str {
11946        match value {
11947            JValue::Null => "null",
11948            JValue::Bool(_) => "boolean",
11949            JValue::Number(_) => "number",
11950            JValue::String(_) => "string",
11951            JValue::Array(_) => "array",
11952            JValue::Object(_) => "object",
11953            #[cfg(feature = "python")]
11954            JValue::LazyPyDict(_) => "object",
11955            _ => "unknown",
11956        }
11957    }
11958
11959    /// Ordered comparison with null/type checking shared across <, <=, >, >=
11960    ///
11961    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
11962    /// `compare_strs` receives (left_str, right_str) for string operands.
11963    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
11964    fn ordered_compare(
11965        &self,
11966        left: &JValue,
11967        right: &JValue,
11968        left_is_explicit_null: bool,
11969        right_is_explicit_null: bool,
11970        op_symbol: &str,
11971        compare_nums: fn(f64, f64) -> bool,
11972        compare_strs: fn(&str, &str) -> bool,
11973    ) -> Result<JValue, EvaluatorError> {
11974        match (left, right) {
11975            (JValue::Number(a), JValue::Number(b)) => {
11976                Ok(JValue::Bool(compare_nums(*a, *b)))
11977            }
11978            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
11979            // Both null/undefined -> return undefined
11980            (JValue::Null, JValue::Null) => Ok(JValue::Null),
11981            // Explicit null literal with any type (except null) -> T2010 error
11982            (JValue::Null, _) if left_is_explicit_null => {
11983                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11984            }
11985            (_, JValue::Null) if right_is_explicit_null => {
11986                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11987            }
11988            // Boolean with undefined -> T2010 error
11989            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
11990                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11991            }
11992            // Number or String with undefined (not explicit null) -> undefined result
11993            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
11994            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
11995                Ok(JValue::Null)
11996            }
11997            // String vs Number -> T2009
11998            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
11999                Err(EvaluatorError::EvaluationError(format!(
12000                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
12001                    op_symbol
12002                )))
12003            }
12004            // Boolean comparisons -> T2010
12005            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
12006                Err(EvaluatorError::EvaluationError(format!(
12007                    "T2010: Cannot compare {} and {}",
12008                    Self::type_name(left), Self::type_name(right)
12009                )))
12010            }
12011            // Other type mismatches
12012            _ => Err(EvaluatorError::EvaluationError(format!(
12013                "T2010: Cannot compare {} and {}",
12014                Self::type_name(left), Self::type_name(right)
12015            ))),
12016        }
12017    }
12018
12019    /// Less than comparison
12020    fn less_than(
12021        &self,
12022        left: &JValue,
12023        right: &JValue,
12024        left_is_explicit_null: bool,
12025        right_is_explicit_null: bool,
12026    ) -> Result<JValue, EvaluatorError> {
12027        self.ordered_compare(
12028            left,
12029            right,
12030            left_is_explicit_null,
12031            right_is_explicit_null,
12032            "<",
12033            |a, b| a < b,
12034            |a, b| a < b,
12035        )
12036    }
12037
12038    /// Less than or equal comparison
12039    fn less_than_or_equal(
12040        &self,
12041        left: &JValue,
12042        right: &JValue,
12043        left_is_explicit_null: bool,
12044        right_is_explicit_null: bool,
12045    ) -> Result<JValue, EvaluatorError> {
12046        self.ordered_compare(
12047            left,
12048            right,
12049            left_is_explicit_null,
12050            right_is_explicit_null,
12051            "<=",
12052            |a, b| a <= b,
12053            |a, b| a <= b,
12054        )
12055    }
12056
12057    /// Greater than comparison
12058    fn greater_than(
12059        &self,
12060        left: &JValue,
12061        right: &JValue,
12062        left_is_explicit_null: bool,
12063        right_is_explicit_null: bool,
12064    ) -> Result<JValue, EvaluatorError> {
12065        self.ordered_compare(
12066            left,
12067            right,
12068            left_is_explicit_null,
12069            right_is_explicit_null,
12070            ">",
12071            |a, b| a > b,
12072            |a, b| a > b,
12073        )
12074    }
12075
12076    /// Greater than or equal comparison
12077    fn greater_than_or_equal(
12078        &self,
12079        left: &JValue,
12080        right: &JValue,
12081        left_is_explicit_null: bool,
12082        right_is_explicit_null: bool,
12083    ) -> Result<JValue, EvaluatorError> {
12084        self.ordered_compare(
12085            left,
12086            right,
12087            left_is_explicit_null,
12088            right_is_explicit_null,
12089            ">=",
12090            |a, b| a >= b,
12091            |a, b| a >= b,
12092        )
12093    }
12094
12095    /// Convert a value to a string for concatenation
12096    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
12097        // Normalize a lazy operand up front: `functions::string::string`'s lazy arm maps a
12098        // conversion failure to `JValue::Null` (silently stringifying to `""`), which would
12099        // swallow the TypeError this must raise instead. Guarded by `is_lazy` so the
12100        // common (non-lazy) path pays no clone.
12101        let normalized;
12102        let value = if value.is_lazy() {
12103            normalized = normalize_lazy(value)?;
12104            &normalized
12105        } else {
12106            value
12107        };
12108        match value {
12109            JValue::String(s) => Ok(s.to_string()),
12110            JValue::Null => Ok(String::new()),
12111            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
12112                match crate::functions::string::string(value, None) {
12113                    Ok(JValue::String(s)) => Ok(s.to_string()),
12114                    Ok(JValue::Null) => Ok(String::new()),
12115                    _ => Err(EvaluatorError::TypeError(
12116                        "Cannot concatenate complex types".to_string(),
12117                    )),
12118                }
12119            }
12120            _ => Ok(String::new()),
12121        }
12122    }
12123
12124    /// String concatenation
12125    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12126        let left_str = Self::value_to_concat_string(left)?;
12127        let right_str = Self::value_to_concat_string(right)?;
12128        Ok(JValue::string(format!("{}{}", left_str, right_str)))
12129    }
12130
12131    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
12132    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12133        // Check left operand is a number or null
12134        let start_f64 = match left {
12135            JValue::Number(n) => Some(*n),
12136            JValue::Null | JValue::Undefined => None,
12137            _ => {
12138                return Err(EvaluatorError::EvaluationError(
12139                    "T2003: Left operand of range operator must be a number".to_string(),
12140                ));
12141            }
12142        };
12143
12144        // Check left operand is an integer (if it's a number)
12145        if let Some(val) = start_f64 {
12146            if val.fract() != 0.0 {
12147                return Err(EvaluatorError::EvaluationError(
12148                    "T2003: Left operand of range operator must be an integer".to_string(),
12149                ));
12150            }
12151        }
12152
12153        // Check right operand is a number or null
12154        let end_f64 = match right {
12155            JValue::Number(n) => Some(*n),
12156            JValue::Null | JValue::Undefined => None,
12157            _ => {
12158                return Err(EvaluatorError::EvaluationError(
12159                    "T2004: Right operand of range operator must be a number".to_string(),
12160                ));
12161            }
12162        };
12163
12164        // Check right operand is an integer (if it's a number)
12165        if let Some(val) = end_f64 {
12166            if val.fract() != 0.0 {
12167                return Err(EvaluatorError::EvaluationError(
12168                    "T2004: Right operand of range operator must be an integer".to_string(),
12169                ));
12170            }
12171        }
12172
12173        // If either operand is null, return empty array
12174        if start_f64.is_none() || end_f64.is_none() {
12175            return Ok(JValue::array(vec![]));
12176        }
12177
12178        let start = start_f64.unwrap() as i64;
12179        let end = end_f64.unwrap() as i64;
12180
12181        // Check range size limit (10 million elements max)
12182        let size = if start <= end {
12183            (end - start + 1) as usize
12184        } else {
12185            0
12186        };
12187        if size > 10_000_000 {
12188            return Err(EvaluatorError::EvaluationError(
12189                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
12190            ));
12191        }
12192        check_sequence_length(size, &self.options)?;
12193
12194        let mut result = Vec::with_capacity(size);
12195        if start <= end {
12196            for i in start..=end {
12197                result.push(JValue::Number(i as f64));
12198            }
12199        }
12200        // Note: if start > end, return empty array (not reversed)
12201        Ok(JValue::array(result))
12202    }
12203
12204    /// In operator (checks if left is in right array/object)
12205    /// Array indexing: array[index]
12206    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
12207        match (array, index) {
12208            (JValue::Array(arr), JValue::Number(n)) => {
12209                let idx = *n as i64;
12210                let len = arr.len() as i64;
12211
12212                // Handle negative indexing (offset from end)
12213                let actual_idx = if idx < 0 { len + idx } else { idx };
12214
12215                if actual_idx < 0 || actual_idx >= len {
12216                    Ok(JValue::Undefined)
12217                } else {
12218                    Ok(arr[actual_idx as usize].clone())
12219                }
12220            }
12221            _ => Err(EvaluatorError::TypeError(
12222                "Array indexing requires array and number".to_string(),
12223            )),
12224        }
12225    }
12226
12227    /// Array filtering: array[predicate]
12228    /// Evaluates the predicate for each item in the array and returns items where predicate is true
12229    fn array_filter(
12230        &mut self,
12231        _lhs_node: &AstNode,
12232        rhs_node: &AstNode,
12233        array: &JValue,
12234        _original_data: &JValue,
12235    ) -> Result<JValue, EvaluatorError> {
12236        match array {
12237            JValue::Array(arr) => {
12238                // Pre-allocate with estimated capacity (assume ~50% will match)
12239                let mut filtered = Vec::with_capacity(arr.len() / 2);
12240
12241                for item in arr.iter() {
12242                    // Evaluate the predicate in the context of this array item
12243                    // The item becomes the new "current context" ($)
12244                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
12245
12246                    // Check if the predicate is truthy
12247                    if self.is_truthy(&predicate_result) {
12248                        filtered.push(item.clone());
12249                    }
12250                }
12251
12252                Ok(JValue::array(filtered))
12253            }
12254            _ => Err(EvaluatorError::TypeError(
12255                "Array filtering requires an array".to_string(),
12256            )),
12257        }
12258    }
12259
12260    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12261        // If either side is undefined/null, return false (not an error)
12262        // This matches JavaScript behavior
12263        if left.is_null() || right.is_null() {
12264            return Ok(JValue::Bool(false));
12265        }
12266
12267        // Normalize a lazy left operand once; every equality below needs a real
12268        // value, not a per-comparison materialization attempt. Guarded by `is_lazy`
12269        // so the common (non-lazy) path pays no clone.
12270        let normalized_left;
12271        let left = if left.is_lazy() {
12272            normalized_left = normalize_lazy(left)?;
12273            &normalized_left
12274        } else {
12275            left
12276        };
12277
12278        match right {
12279            JValue::Array(arr) => {
12280                for v in arr.iter() {
12281                    // compiled_equal normalizes a lazy element (guarded) so a
12282                    // conversion failure raises instead of silently not matching.
12283                    if matches!(compiled_equal(left, v)?, JValue::Bool(true)) {
12284                        return Ok(JValue::Bool(true));
12285                    }
12286                }
12287                Ok(JValue::Bool(false))
12288            }
12289            JValue::Object(obj) => {
12290                if let JValue::String(key) = left {
12291                    Ok(JValue::Bool(obj.contains_key(&**key)))
12292                } else {
12293                    Ok(JValue::Bool(false))
12294                }
12295            }
12296            // Right-side lazy-object key-presence check doesn't convert any values
12297            // (`contains_field` checks the Python dict directly), so it's left as-is.
12298            #[cfg(feature = "python")]
12299            JValue::LazyPyDict(lazy) => {
12300                if let JValue::String(key) = left {
12301                    Ok(JValue::Bool(lazy.contains_field(key)))
12302                } else {
12303                    Ok(JValue::Bool(false))
12304                }
12305            }
12306            // If right side is not an array or object (e.g., string, number),
12307            // wrap it in an array for comparison
12308            other => Ok(JValue::Bool(self.equals(left, other))),
12309        }
12310    }
12311
12312    /// Create a partially applied function from a function call with placeholder arguments
12313    /// This evaluates non-placeholder arguments and creates a new lambda that takes
12314    /// the placeholder positions as parameters.
12315    fn create_partial_application(
12316        &mut self,
12317        name: &str,
12318        args: &[AstNode],
12319        is_builtin: bool,
12320        data: &JValue,
12321    ) -> Result<JValue, EvaluatorError> {
12322        // First, look up the function to ensure it exists
12323        let is_lambda = self.context.lookup_lambda(name).is_some()
12324            || (self
12325                .context
12326                .lookup(name)
12327                .map(|v| matches!(v, JValue::Lambda { .. }))
12328                .unwrap_or(false));
12329
12330        // Built-in functions must be called with $ prefix for partial application
12331        // Without $, it's an error (T1007) suggesting the user forgot the $
12332        if !is_lambda && !is_builtin {
12333            // Check if it's a built-in function called without $
12334            if self.is_builtin_function(name) {
12335                return Err(EvaluatorError::EvaluationError(format!(
12336                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
12337                    name
12338                )));
12339            }
12340            return Err(EvaluatorError::EvaluationError(
12341                "T1008: Attempted to partially apply a non-function".to_string(),
12342            ));
12343        }
12344
12345        // Evaluate non-placeholder arguments and track placeholder positions
12346        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
12347        let mut placeholder_positions: Vec<usize> = Vec::new();
12348
12349        for (i, arg) in args.iter().enumerate() {
12350            if matches!(arg, AstNode::Placeholder) {
12351                placeholder_positions.push(i);
12352            } else {
12353                let value = self.evaluate_internal(arg, data)?;
12354                bound_args.push((i, value));
12355            }
12356        }
12357
12358        // Generate parameter names for each placeholder
12359        let param_names: Vec<String> = placeholder_positions
12360            .iter()
12361            .enumerate()
12362            .map(|(i, _)| format!("__p{}", i))
12363            .collect();
12364
12365        // Store the partial application info as a special lambda
12366        // When invoked, it will call the original function with bound + placeholder args
12367        let partial_id = format!(
12368            "__partial_{}_{}_{}",
12369            name,
12370            placeholder_positions.len(),
12371            bound_args.len()
12372        );
12373
12374        // Create a stored lambda that represents this partial application
12375        // The body is a marker that we'll interpret specially during invocation
12376        let stored_lambda = StoredLambda {
12377            params: param_names.clone(),
12378            body: AstNode::String(format!(
12379                "__partial_call:{}:{}:{}",
12380                name,
12381                is_builtin,
12382                args.len()
12383            )),
12384            compiled_body: None, // Partial application uses a special body marker
12385            signature: None,
12386            captured_env: {
12387                let mut env = self.capture_current_environment();
12388                // Store the bound arguments in the captured environment
12389                for (pos, value) in &bound_args {
12390                    env.insert(format!("__bound_arg_{}", pos), value.clone());
12391                }
12392                // Store placeholder positions
12393                env.insert(
12394                    "__placeholder_positions".to_string(),
12395                    JValue::array(
12396                        placeholder_positions
12397                            .iter()
12398                            .map(|p| JValue::Number(*p as f64))
12399                            .collect::<Vec<_>>(),
12400                    ),
12401                );
12402                // Store total argument count
12403                env.insert(
12404                    "__total_args".to_string(),
12405                    JValue::Number(args.len() as f64),
12406                );
12407                env
12408            },
12409            captured_data: Some(data.clone()),
12410            thunk: false,
12411        };
12412
12413        self.context.bind_lambda(partial_id.clone(), stored_lambda);
12414
12415        // Return a lambda object that can be invoked
12416        let lambda_obj = JValue::lambda(
12417            partial_id.as_str(),
12418            param_names,
12419            Some(name.to_string()),
12420            None::<String>,
12421        );
12422
12423        Ok(lambda_obj)
12424    }
12425}
12426
12427impl Default for Evaluator {
12428    fn default() -> Self {
12429        Self::new()
12430    }
12431}
12432
12433#[cfg(test)]
12434mod tests {
12435    use super::*;
12436    use crate::ast::{BinaryOp, UnaryOp};
12437
12438    // --- Task 7: tuple-wrapper output leak -----------------------------------
12439    //
12440    // `%`/`@`/`#` are implemented internally via a tuple-stream representation
12441    // (`create_tuple_stream`): each element gets wrapped as
12442    // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
12443    // consume/re-wrap these, but the *final* evaluate() result can still carry
12444    // a lingering wrapper -- confirmed for real by dumping actual output before
12445    // this fix (see task-7-report.md for the raw before/after). These tests
12446    // pin both the bare top-level case (Task 5's brief `#` example) and the
12447    // object/array-construction-nested case (found while verifying the brief's
12448    // illustrative fix against real output -- a plain per-element Array-only
12449    // recursion does not reach into a constructed object's field values).
12450
12451    fn dataset5_for_tuple_tests() -> JValue {
12452        let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
12453        serde_json::from_str::<serde_json::Value>(s).unwrap().into()
12454    }
12455
12456    fn assert_no_tuple_wrapper(value: &JValue) {
12457        match value {
12458            JValue::Object(obj) => {
12459                assert!(
12460                    obj.get("__tuple__").is_none(),
12461                    "tuple wrapper leaked into output: {:?}",
12462                    value
12463                );
12464                for v in obj.values() {
12465                    assert_no_tuple_wrapper(v);
12466                }
12467            }
12468            JValue::Array(arr) => {
12469                for item in arr.iter() {
12470                    assert_no_tuple_wrapper(item);
12471                }
12472            }
12473            _ => {}
12474        }
12475    }
12476
12477    #[test]
12478    fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
12479        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12480        let ast = crate::parser::parse("items#$i").unwrap();
12481        let mut evaluator = Evaluator::new();
12482        let result = evaluator.evaluate(&ast, &data).unwrap();
12483        assert_no_tuple_wrapper(&result);
12484        assert_eq!(
12485            result,
12486            JValue::array(vec![
12487                JValue::from(1i64),
12488                JValue::from(2i64),
12489                JValue::from(3i64)
12490            ])
12491        );
12492    }
12493
12494    #[test]
12495    fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
12496        // Confirmed by Task 6 to evaluate to the correct @-values but stay
12497        // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
12498        let data = dataset5_for_tuple_tests();
12499        let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
12500        let mut evaluator = Evaluator::new();
12501        let result = evaluator.evaluate(&ast, &data).unwrap();
12502        assert_no_tuple_wrapper(&result);
12503        assert_eq!(
12504            result,
12505            JValue::array(vec![
12506                JValue::string("040657863"),
12507                JValue::string("0406654603"),
12508            ])
12509        );
12510    }
12511
12512    #[test]
12513    fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
12514        // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
12515        let data = dataset5_for_tuple_tests();
12516        let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
12517            .unwrap();
12518        let mut evaluator = Evaluator::new();
12519        let result = evaluator.evaluate(&ast, &data).unwrap();
12520        assert_no_tuple_wrapper(&result);
12521        assert_eq!(
12522            result,
12523            JValue::array(vec![
12524                JValue::string("0406654608"),
12525                JValue::string("0406634348"),
12526            ])
12527        );
12528    }
12529
12530    #[test]
12531    fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
12532        // A tuple-producing expression nested inside a constructed object's field
12533        // value: the top-level result is a plain (non-tuple) Object, so a naive
12534        // "unwrap only if the whole value is a tuple wrapper" check would miss
12535        // this -- must recurse into field values too.
12536        let data = dataset5_for_tuple_tests();
12537        let ast =
12538            crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
12539                .unwrap();
12540        let mut evaluator = Evaluator::new();
12541        let result = evaluator.evaluate(&ast, &data).unwrap();
12542        assert_no_tuple_wrapper(&result);
12543        assert_eq!(
12544            result,
12545            JValue::from(serde_json::json!({
12546                "skus": ["040657863", "0406654603"]
12547            }))
12548        );
12549    }
12550
12551    #[test]
12552    fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
12553        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12554        let ast = crate::parser::parse("[items#$i]").unwrap();
12555        let mut evaluator = Evaluator::new();
12556        let result = evaluator.evaluate(&ast, &data).unwrap();
12557        assert_no_tuple_wrapper(&result);
12558    }
12559
12560    #[test]
12561    fn test_evaluate_literals() {
12562        let mut evaluator = Evaluator::new();
12563        let data = JValue::Null;
12564
12565        // String literal
12566        let result = evaluator
12567            .evaluate(&AstNode::string("hello"), &data)
12568            .unwrap();
12569        assert_eq!(result, JValue::string("hello"));
12570
12571        // Number literal
12572        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
12573        assert_eq!(result, JValue::from(42i64));
12574
12575        // Boolean literal
12576        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
12577        assert_eq!(result, JValue::Bool(true));
12578
12579        // Null literal
12580        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
12581        assert_eq!(result, JValue::Null);
12582    }
12583
12584    #[test]
12585    fn test_evaluate_variables() {
12586        let mut evaluator = Evaluator::new();
12587        let data = JValue::Null;
12588
12589        // Bind a variable
12590        evaluator
12591            .context
12592            .bind("x".to_string(), JValue::from(100i64));
12593
12594        // Look up the variable
12595        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
12596        assert_eq!(result, JValue::from(100i64));
12597
12598        // Undefined variable returns null (undefined in JSONata semantics)
12599        let result = evaluator
12600            .evaluate(&AstNode::variable("undefined"), &data)
12601            .unwrap();
12602        assert_eq!(result, JValue::Null);
12603    }
12604
12605    #[test]
12606    fn test_evaluate_path() {
12607        let mut evaluator = Evaluator::new();
12608        let data = JValue::from(serde_json::json!({
12609            "foo": {
12610                "bar": {
12611                    "baz": 42
12612                }
12613            }
12614        }));
12615        // Simple path
12616        let path = AstNode::Path {
12617            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
12618        };
12619        let result = evaluator.evaluate(&path, &data).unwrap();
12620        assert_eq!(
12621            result,
12622            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
12623        );
12624
12625        // Nested path
12626        let path = AstNode::Path {
12627            steps: vec![
12628                PathStep::new(AstNode::Name("foo".to_string())),
12629                PathStep::new(AstNode::Name("bar".to_string())),
12630                PathStep::new(AstNode::Name("baz".to_string())),
12631            ],
12632        };
12633        let result = evaluator.evaluate(&path, &data).unwrap();
12634        assert_eq!(result, JValue::from(42i64));
12635
12636        // Missing path returns undefined (not null - see issue #32)
12637        let path = AstNode::Path {
12638            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
12639        };
12640        let result = evaluator.evaluate(&path, &data).unwrap();
12641        assert_eq!(result, JValue::Undefined);
12642    }
12643
12644    #[test]
12645    fn test_arithmetic_operations() {
12646        let mut evaluator = Evaluator::new();
12647        let data = JValue::Null;
12648
12649        // Addition
12650        let expr = AstNode::Binary {
12651            op: BinaryOp::Add,
12652            lhs: Box::new(AstNode::number(10.0)),
12653            rhs: Box::new(AstNode::number(5.0)),
12654        };
12655        let result = evaluator.evaluate(&expr, &data).unwrap();
12656        assert_eq!(result, JValue::Number(15.0));
12657
12658        // Subtraction
12659        let expr = AstNode::Binary {
12660            op: BinaryOp::Subtract,
12661            lhs: Box::new(AstNode::number(10.0)),
12662            rhs: Box::new(AstNode::number(5.0)),
12663        };
12664        let result = evaluator.evaluate(&expr, &data).unwrap();
12665        assert_eq!(result, JValue::Number(5.0));
12666
12667        // Multiplication
12668        let expr = AstNode::Binary {
12669            op: BinaryOp::Multiply,
12670            lhs: Box::new(AstNode::number(10.0)),
12671            rhs: Box::new(AstNode::number(5.0)),
12672        };
12673        let result = evaluator.evaluate(&expr, &data).unwrap();
12674        assert_eq!(result, JValue::Number(50.0));
12675
12676        // Division
12677        let expr = AstNode::Binary {
12678            op: BinaryOp::Divide,
12679            lhs: Box::new(AstNode::number(10.0)),
12680            rhs: Box::new(AstNode::number(5.0)),
12681        };
12682        let result = evaluator.evaluate(&expr, &data).unwrap();
12683        assert_eq!(result, JValue::Number(2.0));
12684
12685        // Modulo
12686        let expr = AstNode::Binary {
12687            op: BinaryOp::Modulo,
12688            lhs: Box::new(AstNode::number(10.0)),
12689            rhs: Box::new(AstNode::number(3.0)),
12690        };
12691        let result = evaluator.evaluate(&expr, &data).unwrap();
12692        assert_eq!(result, JValue::Number(1.0));
12693    }
12694
12695    #[test]
12696    fn test_division_by_zero() {
12697        let mut evaluator = Evaluator::new();
12698        let data = JValue::Null;
12699
12700        let expr = AstNode::Binary {
12701            op: BinaryOp::Divide,
12702            lhs: Box::new(AstNode::number(10.0)),
12703            rhs: Box::new(AstNode::number(0.0)),
12704        };
12705        let result = evaluator.evaluate(&expr, &data);
12706        assert!(result.is_err());
12707    }
12708
12709    #[test]
12710    fn test_comparison_operations() {
12711        let mut evaluator = Evaluator::new();
12712        let data = JValue::Null;
12713
12714        // Equal
12715        let expr = AstNode::Binary {
12716            op: BinaryOp::Equal,
12717            lhs: Box::new(AstNode::number(5.0)),
12718            rhs: Box::new(AstNode::number(5.0)),
12719        };
12720        assert_eq!(
12721            evaluator.evaluate(&expr, &data).unwrap(),
12722            JValue::Bool(true)
12723        );
12724
12725        // Not equal
12726        let expr = AstNode::Binary {
12727            op: BinaryOp::NotEqual,
12728            lhs: Box::new(AstNode::number(5.0)),
12729            rhs: Box::new(AstNode::number(3.0)),
12730        };
12731        assert_eq!(
12732            evaluator.evaluate(&expr, &data).unwrap(),
12733            JValue::Bool(true)
12734        );
12735
12736        // Less than
12737        let expr = AstNode::Binary {
12738            op: BinaryOp::LessThan,
12739            lhs: Box::new(AstNode::number(3.0)),
12740            rhs: Box::new(AstNode::number(5.0)),
12741        };
12742        assert_eq!(
12743            evaluator.evaluate(&expr, &data).unwrap(),
12744            JValue::Bool(true)
12745        );
12746
12747        // Greater than
12748        let expr = AstNode::Binary {
12749            op: BinaryOp::GreaterThan,
12750            lhs: Box::new(AstNode::number(5.0)),
12751            rhs: Box::new(AstNode::number(3.0)),
12752        };
12753        assert_eq!(
12754            evaluator.evaluate(&expr, &data).unwrap(),
12755            JValue::Bool(true)
12756        );
12757    }
12758
12759    #[test]
12760    fn test_logical_operations() {
12761        let mut evaluator = Evaluator::new();
12762        let data = JValue::Null;
12763
12764        // And - both true
12765        let expr = AstNode::Binary {
12766            op: BinaryOp::And,
12767            lhs: Box::new(AstNode::boolean(true)),
12768            rhs: Box::new(AstNode::boolean(true)),
12769        };
12770        assert_eq!(
12771            evaluator.evaluate(&expr, &data).unwrap(),
12772            JValue::Bool(true)
12773        );
12774
12775        // And - first false
12776        let expr = AstNode::Binary {
12777            op: BinaryOp::And,
12778            lhs: Box::new(AstNode::boolean(false)),
12779            rhs: Box::new(AstNode::boolean(true)),
12780        };
12781        assert_eq!(
12782            evaluator.evaluate(&expr, &data).unwrap(),
12783            JValue::Bool(false)
12784        );
12785
12786        // Or - first true
12787        let expr = AstNode::Binary {
12788            op: BinaryOp::Or,
12789            lhs: Box::new(AstNode::boolean(true)),
12790            rhs: Box::new(AstNode::boolean(false)),
12791        };
12792        assert_eq!(
12793            evaluator.evaluate(&expr, &data).unwrap(),
12794            JValue::Bool(true)
12795        );
12796
12797        // Or - both false
12798        let expr = AstNode::Binary {
12799            op: BinaryOp::Or,
12800            lhs: Box::new(AstNode::boolean(false)),
12801            rhs: Box::new(AstNode::boolean(false)),
12802        };
12803        assert_eq!(
12804            evaluator.evaluate(&expr, &data).unwrap(),
12805            JValue::Bool(false)
12806        );
12807    }
12808
12809    #[test]
12810    fn test_string_concatenation() {
12811        let mut evaluator = Evaluator::new();
12812        let data = JValue::Null;
12813
12814        let expr = AstNode::Binary {
12815            op: BinaryOp::Concatenate,
12816            lhs: Box::new(AstNode::string("Hello")),
12817            rhs: Box::new(AstNode::string(" World")),
12818        };
12819        let result = evaluator.evaluate(&expr, &data).unwrap();
12820        assert_eq!(result, JValue::string("Hello World"));
12821    }
12822
12823    #[test]
12824    fn test_range_operator() {
12825        let mut evaluator = Evaluator::new();
12826        let data = JValue::Null;
12827
12828        // Forward range
12829        let expr = AstNode::Binary {
12830            op: BinaryOp::Range,
12831            lhs: Box::new(AstNode::number(1.0)),
12832            rhs: Box::new(AstNode::number(5.0)),
12833        };
12834        let result = evaluator.evaluate(&expr, &data).unwrap();
12835        assert_eq!(
12836            result,
12837            JValue::array(vec![
12838                JValue::Number(1.0),
12839                JValue::Number(2.0),
12840                JValue::Number(3.0),
12841                JValue::Number(4.0),
12842                JValue::Number(5.0)
12843            ])
12844        );
12845
12846        // Backward range (start > end) returns empty array
12847        let expr = AstNode::Binary {
12848            op: BinaryOp::Range,
12849            lhs: Box::new(AstNode::number(5.0)),
12850            rhs: Box::new(AstNode::number(1.0)),
12851        };
12852        let result = evaluator.evaluate(&expr, &data).unwrap();
12853        assert_eq!(result, JValue::array(vec![]));
12854    }
12855
12856    #[test]
12857    fn test_in_operator() {
12858        let mut evaluator = Evaluator::new();
12859        let data = JValue::Null;
12860
12861        // In array
12862        let expr = AstNode::Binary {
12863            op: BinaryOp::In,
12864            lhs: Box::new(AstNode::number(3.0)),
12865            rhs: Box::new(AstNode::Array(vec![
12866                AstNode::number(1.0),
12867                AstNode::number(2.0),
12868                AstNode::number(3.0),
12869            ])),
12870        };
12871        let result = evaluator.evaluate(&expr, &data).unwrap();
12872        assert_eq!(result, JValue::Bool(true));
12873
12874        // Not in array
12875        let expr = AstNode::Binary {
12876            op: BinaryOp::In,
12877            lhs: Box::new(AstNode::number(5.0)),
12878            rhs: Box::new(AstNode::Array(vec![
12879                AstNode::number(1.0),
12880                AstNode::number(2.0),
12881                AstNode::number(3.0),
12882            ])),
12883        };
12884        let result = evaluator.evaluate(&expr, &data).unwrap();
12885        assert_eq!(result, JValue::Bool(false));
12886    }
12887
12888    #[test]
12889    fn test_unary_operations() {
12890        let mut evaluator = Evaluator::new();
12891        let data = JValue::Null;
12892
12893        // Negation
12894        let expr = AstNode::Unary {
12895            op: UnaryOp::Negate,
12896            operand: Box::new(AstNode::number(5.0)),
12897        };
12898        let result = evaluator.evaluate(&expr, &data).unwrap();
12899        assert_eq!(result, JValue::Number(-5.0));
12900
12901        // Not
12902        let expr = AstNode::Unary {
12903            op: UnaryOp::Not,
12904            operand: Box::new(AstNode::boolean(true)),
12905        };
12906        let result = evaluator.evaluate(&expr, &data).unwrap();
12907        assert_eq!(result, JValue::Bool(false));
12908    }
12909
12910    #[test]
12911    fn test_array_construction() {
12912        let mut evaluator = Evaluator::new();
12913        let data = JValue::Null;
12914
12915        let expr = AstNode::Array(vec![
12916            AstNode::number(1.0),
12917            AstNode::number(2.0),
12918            AstNode::number(3.0),
12919        ]);
12920        let result = evaluator.evaluate(&expr, &data).unwrap();
12921        // Whole number literals are preserved as integers
12922        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
12923    }
12924
12925    #[test]
12926    fn test_object_construction() {
12927        let mut evaluator = Evaluator::new();
12928        let data = JValue::Null;
12929
12930        let expr = AstNode::Object(vec![
12931            (AstNode::string("name"), AstNode::string("Alice")),
12932            (AstNode::string("age"), AstNode::number(30.0)),
12933        ]);
12934        let result = evaluator.evaluate(&expr, &data).unwrap();
12935        // Whole number literals are preserved as integers
12936        let mut expected = IndexMap::new();
12937        expected.insert("name".to_string(), JValue::string("Alice"));
12938        expected.insert("age".to_string(), JValue::Number(30.0));
12939        assert_eq!(result, JValue::object(expected));
12940    }
12941
12942    #[test]
12943    fn test_conditional() {
12944        let mut evaluator = Evaluator::new();
12945        let data = JValue::Null;
12946
12947        // True condition
12948        let expr = AstNode::Conditional {
12949            condition: Box::new(AstNode::boolean(true)),
12950            then_branch: Box::new(AstNode::string("yes")),
12951            else_branch: Some(Box::new(AstNode::string("no"))),
12952        };
12953        let result = evaluator.evaluate(&expr, &data).unwrap();
12954        assert_eq!(result, JValue::string("yes"));
12955
12956        // False condition
12957        let expr = AstNode::Conditional {
12958            condition: Box::new(AstNode::boolean(false)),
12959            then_branch: Box::new(AstNode::string("yes")),
12960            else_branch: Some(Box::new(AstNode::string("no"))),
12961        };
12962        let result = evaluator.evaluate(&expr, &data).unwrap();
12963        assert_eq!(result, JValue::string("no"));
12964
12965        // No else branch returns undefined (not null)
12966        let expr = AstNode::Conditional {
12967            condition: Box::new(AstNode::boolean(false)),
12968            then_branch: Box::new(AstNode::string("yes")),
12969            else_branch: None,
12970        };
12971        let result = evaluator.evaluate(&expr, &data).unwrap();
12972        assert_eq!(result, JValue::Undefined);
12973    }
12974
12975    #[test]
12976    fn test_block_expression() {
12977        let mut evaluator = Evaluator::new();
12978        let data = JValue::Null;
12979
12980        let expr = AstNode::Block(vec![
12981            AstNode::number(1.0),
12982            AstNode::number(2.0),
12983            AstNode::number(3.0),
12984        ]);
12985        let result = evaluator.evaluate(&expr, &data).unwrap();
12986        // Block returns the last expression; whole numbers are preserved as integers
12987        assert_eq!(result, JValue::from(3i64));
12988    }
12989
12990    #[test]
12991    fn test_function_calls() {
12992        let mut evaluator = Evaluator::new();
12993        let data = JValue::Null;
12994
12995        // uppercase function
12996        let expr = AstNode::Function {
12997            name: "uppercase".to_string(),
12998            args: vec![AstNode::string("hello")],
12999            is_builtin: true,
13000        };
13001        let result = evaluator.evaluate(&expr, &data).unwrap();
13002        assert_eq!(result, JValue::string("HELLO"));
13003
13004        // lowercase function
13005        let expr = AstNode::Function {
13006            name: "lowercase".to_string(),
13007            args: vec![AstNode::string("HELLO")],
13008            is_builtin: true,
13009        };
13010        let result = evaluator.evaluate(&expr, &data).unwrap();
13011        assert_eq!(result, JValue::string("hello"));
13012
13013        // length function
13014        let expr = AstNode::Function {
13015            name: "length".to_string(),
13016            args: vec![AstNode::string("hello")],
13017            is_builtin: true,
13018        };
13019        let result = evaluator.evaluate(&expr, &data).unwrap();
13020        assert_eq!(result, JValue::from(5i64));
13021
13022        // sum function
13023        let expr = AstNode::Function {
13024            name: "sum".to_string(),
13025            args: vec![AstNode::Array(vec![
13026                AstNode::number(1.0),
13027                AstNode::number(2.0),
13028                AstNode::number(3.0),
13029            ])],
13030            is_builtin: true,
13031        };
13032        let result = evaluator.evaluate(&expr, &data).unwrap();
13033        assert_eq!(result, JValue::Number(6.0));
13034
13035        // count function
13036        let expr = AstNode::Function {
13037            name: "count".to_string(),
13038            args: vec![AstNode::Array(vec![
13039                AstNode::number(1.0),
13040                AstNode::number(2.0),
13041                AstNode::number(3.0),
13042            ])],
13043            is_builtin: true,
13044        };
13045        let result = evaluator.evaluate(&expr, &data).unwrap();
13046        assert_eq!(result, JValue::from(3i64));
13047    }
13048
13049    #[test]
13050    fn test_complex_nested_data() {
13051        let mut evaluator = Evaluator::new();
13052        let data = JValue::from(serde_json::json!({
13053            "users": [
13054                {"name": "Alice", "age": 30},
13055                {"name": "Bob", "age": 25},
13056                {"name": "Charlie", "age": 35}
13057            ],
13058            "metadata": {
13059                "total": 3,
13060                "version": "1.0"
13061            }
13062        }));
13063        // Access nested field
13064        let path = AstNode::Path {
13065            steps: vec![
13066                PathStep::new(AstNode::Name("metadata".to_string())),
13067                PathStep::new(AstNode::Name("version".to_string())),
13068            ],
13069        };
13070        let result = evaluator.evaluate(&path, &data).unwrap();
13071        assert_eq!(result, JValue::string("1.0"));
13072    }
13073
13074    #[test]
13075    fn test_error_handling() {
13076        let mut evaluator = Evaluator::new();
13077        let data = JValue::Null;
13078
13079        // Type error: adding string and number
13080        let expr = AstNode::Binary {
13081            op: BinaryOp::Add,
13082            lhs: Box::new(AstNode::string("hello")),
13083            rhs: Box::new(AstNode::number(5.0)),
13084        };
13085        let result = evaluator.evaluate(&expr, &data);
13086        assert!(result.is_err());
13087
13088        // Reference error: undefined function
13089        let expr = AstNode::Function {
13090            name: "undefined_function".to_string(),
13091            args: vec![],
13092            is_builtin: false,
13093        };
13094        let result = evaluator.evaluate(&expr, &data);
13095        assert!(result.is_err());
13096    }
13097
13098    #[test]
13099    fn test_truthiness() {
13100        let evaluator = Evaluator::new();
13101
13102        assert!(!evaluator.is_truthy(&JValue::Null));
13103        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
13104        assert!(evaluator.is_truthy(&JValue::Bool(true)));
13105        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
13106        assert!(evaluator.is_truthy(&JValue::from(1i64)));
13107        assert!(!evaluator.is_truthy(&JValue::string("")));
13108        assert!(evaluator.is_truthy(&JValue::string("hello")));
13109        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
13110        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
13111    }
13112
13113    #[test]
13114    fn test_integration_with_parser() {
13115        use crate::parser::parse;
13116
13117        let mut evaluator = Evaluator::new();
13118        let data = JValue::from(serde_json::json!({
13119            "price": 10,
13120            "quantity": 5
13121        }));
13122        // Test simple path
13123        let ast = parse("price").unwrap();
13124        let result = evaluator.evaluate(&ast, &data).unwrap();
13125        assert_eq!(result, JValue::from(10i64));
13126
13127        // Test arithmetic
13128        let ast = parse("price * quantity").unwrap();
13129        let result = evaluator.evaluate(&ast, &data).unwrap();
13130        // Note: Arithmetic operations produce f64 results in JSON
13131        assert_eq!(result, JValue::Number(50.0));
13132
13133        // Test comparison
13134        let ast = parse("price > 5").unwrap();
13135        let result = evaluator.evaluate(&ast, &data).unwrap();
13136        assert_eq!(result, JValue::Bool(true));
13137    }
13138
13139    #[test]
13140    fn test_evaluate_dollar_function_uppercase() {
13141        use crate::parser::parse;
13142
13143        let mut evaluator = Evaluator::new();
13144        let ast = parse(r#"$uppercase("hello")"#).unwrap();
13145        let empty = JValue::object(IndexMap::new());
13146        let result = evaluator.evaluate(&ast, &empty).unwrap();
13147        assert_eq!(result, JValue::string("HELLO"));
13148    }
13149
13150    #[test]
13151    fn test_evaluate_dollar_function_sum() {
13152        use crate::parser::parse;
13153
13154        let mut evaluator = Evaluator::new();
13155        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
13156        let empty = JValue::object(IndexMap::new());
13157        let result = evaluator.evaluate(&ast, &empty).unwrap();
13158        assert_eq!(result, JValue::Number(15.0));
13159    }
13160
13161    #[test]
13162    fn test_evaluate_nested_dollar_functions() {
13163        use crate::parser::parse;
13164
13165        let mut evaluator = Evaluator::new();
13166        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
13167        let empty = JValue::object(IndexMap::new());
13168        let result = evaluator.evaluate(&ast, &empty).unwrap();
13169        // length() returns an integer, not a float
13170        assert_eq!(result, JValue::Number(5.0));
13171    }
13172
13173    #[test]
13174    fn test_array_mapping() {
13175        use crate::parser::parse;
13176
13177        let mut evaluator = Evaluator::new();
13178        let data: JValue = serde_json::from_str(
13179            r#"{
13180            "products": [
13181                {"id": 1, "name": "Laptop", "price": 999.99},
13182                {"id": 2, "name": "Mouse", "price": 29.99},
13183                {"id": 3, "name": "Keyboard", "price": 79.99}
13184            ]
13185        }"#,
13186        )
13187        .map(|v: serde_json::Value| JValue::from(v))
13188        .unwrap();
13189
13190        // Test mapping over array to extract field
13191        let ast = parse("products.name").unwrap();
13192        let result = evaluator.evaluate(&ast, &data).unwrap();
13193        assert_eq!(
13194            result,
13195            JValue::array(vec![
13196                JValue::string("Laptop"),
13197                JValue::string("Mouse"),
13198                JValue::string("Keyboard")
13199            ])
13200        );
13201
13202        // Test mapping over array to extract prices
13203        let ast = parse("products.price").unwrap();
13204        let result = evaluator.evaluate(&ast, &data).unwrap();
13205        assert_eq!(
13206            result,
13207            JValue::array(vec![
13208                JValue::Number(999.99),
13209                JValue::Number(29.99),
13210                JValue::Number(79.99)
13211            ])
13212        );
13213
13214        // Test with $sum function on mapped array
13215        let ast = parse("$sum(products.price)").unwrap();
13216        let result = evaluator.evaluate(&ast, &data).unwrap();
13217        assert_eq!(result, JValue::Number(1109.97));
13218    }
13219
13220    #[test]
13221    fn test_empty_brackets() {
13222        use crate::parser::parse;
13223
13224        let mut evaluator = Evaluator::new();
13225
13226        // Test empty brackets on simple value - should wrap in array
13227        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
13228        let ast = parse("foo[]").unwrap();
13229        let result = evaluator.evaluate(&ast, &data).unwrap();
13230        assert_eq!(
13231            result,
13232            JValue::array(vec![JValue::string("bar")]),
13233            "Empty brackets should wrap value in array"
13234        );
13235
13236        // Test empty brackets on array - should return array as-is
13237        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
13238        let ast2 = parse("arr[]").unwrap();
13239        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
13240        assert_eq!(
13241            result2,
13242            JValue::array(vec![
13243                JValue::Number(1.0),
13244                JValue::Number(2.0),
13245                JValue::Number(3.0)
13246            ]),
13247            "Empty brackets should preserve array"
13248        );
13249    }
13250
13251    // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
13252    // Expected values below are ground-truthed against jsonata-js 2.x.
13253
13254    #[test]
13255    fn test_index_bind_makes_variable_available_in_next_step() {
13256        // `#$o` binds each Order's position; `$o` must resolve in the later step.
13257        let data: JValue = serde_json::json!({
13258            "Account": {
13259                "Order": [
13260                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
13261                    {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
13262                ]
13263            }
13264        })
13265        .into();
13266        let ast =
13267            crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
13268        let mut evaluator = Evaluator::new();
13269        let result = evaluator.evaluate(&ast, &data).unwrap();
13270        assert_eq!(
13271            result,
13272            serde_json::json!([
13273                {"name": "Hat", "idx": 0},
13274                {"name": "Cap", "idx": 1},
13275                {"name": "Sock", "idx": 1}
13276            ])
13277            .into()
13278        );
13279    }
13280
13281    #[test]
13282    fn test_index_bind_with_predicate_stage() {
13283        // Mirrors reference joins/index[13]: index binding, then a predicate on
13284        // the next step, carrying the index binding through.
13285        let data: JValue = serde_json::json!({
13286            "Account": {
13287                "Order": [
13288                    {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
13289                    {"Product": [{"ProductID": 9, "Name": "C"}]}
13290                ]
13291            }
13292        })
13293        .into();
13294        let ast =
13295            crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
13296                .unwrap();
13297        let mut evaluator = Evaluator::new();
13298        let result = evaluator.evaluate(&ast, &data).unwrap();
13299        assert_eq!(
13300            result,
13301            serde_json::json!([
13302                {"n": "B", "idx": 0},
13303                {"n": "C", "idx": 1}
13304            ])
13305            .into()
13306        );
13307    }
13308
13309    #[test]
13310    fn test_focus_bind_makes_variable_available_in_next_step() {
13311        // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
13312        // does NOT advance the context `@`); the variable itself is what carries
13313        // forward. This asserts the real jsonata-js behaviour.
13314        let data: JValue = serde_json::json!({
13315            "Account": {
13316                "Order": [
13317                    {"OrderID": "o1"},
13318                    {"OrderID": "o2"}
13319                ]
13320            }
13321        })
13322        .into();
13323        let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
13324        let mut evaluator = Evaluator::new();
13325        let result = evaluator.evaluate(&ast, &data).unwrap();
13326        assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
13327    }
13328
13329    #[test]
13330    fn test_parent_reference_resolves_to_enclosing_step_value() {
13331        let data: JValue = serde_json::json!({
13332            "Account": {
13333                "Order": [
13334                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
13335                ]
13336            }
13337        })
13338        .into();
13339        let ast =
13340            crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
13341                .unwrap();
13342        let mut evaluator = Evaluator::new();
13343        let result = evaluator.evaluate(&ast, &data).unwrap();
13344        assert_eq!(
13345            result,
13346            serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
13347        );
13348    }
13349
13350    // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
13351    // a tuple-carried `$name`/`!label` key straight into the top scope and then
13352    // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
13353    // same-named outer `:=` binding that happened to be live in that scope
13354    // frame. Expected values below are verified against jsonata-js (2.2.1
13355    // reference, `tests/jsonata-js`).
13356
13357    #[test]
13358    fn test_chained_focus_bind_does_not_clobber_outer_variable() {
13359        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13360        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
13361        let mut evaluator = Evaluator::new();
13362        let result = evaluator.evaluate(&ast, &data).unwrap();
13363        assert_eq!(result, serde_json::json!("OUT").into());
13364    }
13365
13366    #[test]
13367    fn test_chained_index_bind_does_not_clobber_outer_variable() {
13368        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13369        let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
13370        let mut evaluator = Evaluator::new();
13371        let result = evaluator.evaluate(&ast, &data).unwrap();
13372        assert_eq!(result, serde_json::json!("OUT").into());
13373    }
13374
13375    #[test]
13376    fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
13377        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13378        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
13379        let mut evaluator = Evaluator::new();
13380        let result = evaluator.evaluate(&ast, &data).unwrap();
13381        assert_eq!(result, serde_json::json!("OUT").into());
13382    }
13383
13384    #[test]
13385    fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
13386        let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
13387        let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
13388        let mut evaluator = Evaluator::new();
13389        let result = evaluator.evaluate(&ast, &data).unwrap();
13390        assert_eq!(result, serde_json::json!("OUT").into());
13391    }
13392}