Skip to main content

jsonata_core/
evaluator.rs

1// Expression evaluator
2// Mirrors jsonata.js from the reference implementation
3
4#![allow(clippy::cloned_ref_to_slice_refs)]
5#![allow(clippy::explicit_counter_loop)]
6#![allow(clippy::too_many_arguments)]
7#![allow(clippy::manual_strip)]
8
9use std::cmp::Ordering;
10use std::collections::{HashMap, HashSet};
11
12use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
13use crate::parser;
14use crate::value::JValue;
15use indexmap::IndexMap;
16use std::rc::Rc;
17use thiserror::Error;
18
19/// Specialized sort comparator for `$l.field op $r.field` patterns.
20/// Bypasses the full AST evaluator for simple field-based sort comparisons.
21///
22/// In JSONata `$sort`, the comparator returns true when `$l` should come AFTER `$r`.
23/// `$l.field > $r.field` swaps when left > right, producing ascending order.
24/// `$l.field < $r.field` swaps when left < right, producing descending order.
25struct SpecializedSortComparator {
26    field: String,
27    descending: bool,
28}
29
30/// Pre-extracted sort key for the Schwartzian transform in specialized sorting.
31enum SortKey {
32    Num(f64),
33    Str(Rc<str>),
34    None,
35}
36
37fn compare_sort_keys(a: &SortKey, b: &SortKey, descending: bool) -> Ordering {
38    let ord = match (a, b) {
39        (SortKey::Num(x), SortKey::Num(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
40        (SortKey::Str(x), SortKey::Str(y)) => (**x).cmp(&**y),
41        (SortKey::None, SortKey::None) => Ordering::Equal,
42        (SortKey::None, _) => Ordering::Greater,
43        (_, SortKey::None) => Ordering::Less,
44        // Mixed types: maintain original order
45        _ => Ordering::Equal,
46    };
47    if descending {
48        ord.reverse()
49    } else {
50        ord
51    }
52}
53
54/// Try to extract a specialized sort comparator from a lambda AST node.
55/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
56fn try_specialize_sort_comparator(
57    body: &AstNode,
58    left_param: &str,
59    right_param: &str,
60) -> Option<SpecializedSortComparator> {
61    let AstNode::Binary { op, lhs, rhs } = body else {
62        return None;
63    };
64
65    // Returns true if op means "swap when left > right" (ascending order).
66    let is_ascending = |op: &BinaryOp| -> Option<bool> {
67        match op {
68            BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
69            BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
70            _ => None,
71        }
72    };
73
74    // Extract field name from a `$param.field` path with no stages.
75    let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
76        let AstNode::Path { steps } = node else {
77            return None;
78        };
79        if steps.len() != 2 {
80            return None;
81        }
82        let AstNode::Variable(var) = &steps[0].node else {
83            return None;
84        };
85        if var != param {
86            return None;
87        }
88        let AstNode::Name(field) = &steps[1].node else {
89            return None;
90        };
91        if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() {
92            return None;
93        }
94        Some(field.clone())
95    };
96
97    // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
98    for flipped in [false, true] {
99        let (lhs_param, rhs_param) = if flipped {
100            (right_param, left_param)
101        } else {
102            (left_param, right_param)
103        };
104        if let (Some(lhs_field), Some(rhs_field)) = (
105            extract_var_field(lhs, lhs_param),
106            extract_var_field(rhs, rhs_param),
107        ) {
108            if lhs_field == rhs_field {
109                let descending = match op {
110                    // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
111                    // Flipped `$r.f - $l.f` → positive when r > l → descending.
112                    BinaryOp::Subtract => flipped,
113                    // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
114                    _ => {
115                        let ascending = is_ascending(op)?;
116                        if flipped {
117                            ascending
118                        } else {
119                            !ascending
120                        }
121                    }
122                };
123                return Some(SpecializedSortComparator {
124                    field: lhs_field,
125                    descending,
126                });
127            }
128        }
129    }
130    None
131}
132
133// ──────────────────────────────────────────────────────────────────────────────
134// CompiledExpr — unified compiled expression framework
135// ──────────────────────────────────────────────────────────────────────────────
136//
137// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
138// can represent arbitrary simple expressions without AST walking.  Evaluated in
139// a tight loop with no recursion tracking, no scope management, and no AstNode
140// pattern matching.
141
142/// Shape cache: maps field names to their positional index in an IndexMap.
143/// When all objects in an array share the same key ordering (extremely common
144/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
145/// instead of O(1)-amortized hash lookups.
146type ShapeCache = HashMap<String, usize>;
147
148/// Build a shape cache from the first object in an array.
149/// Returns None if the data is not an object.
150fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
151    match first_element {
152        JValue::Object(obj) => {
153            let mut cache = HashMap::with_capacity(obj.len());
154            for (idx, (key, _)) in obj.iter().enumerate() {
155                cache.insert(key.clone(), idx);
156            }
157            Some(cache)
158        }
159        _ => None,
160    }
161}
162
163/// Comparison operator for compiled expressions.
164#[derive(Debug, Clone, Copy)]
165pub(crate) enum CompiledCmp {
166    Eq,
167    Ne,
168    Lt,
169    Le,
170    Gt,
171    Ge,
172}
173
174/// Arithmetic operator for compiled expressions.
175#[derive(Debug, Clone, Copy)]
176pub(crate) enum CompiledArithOp {
177    Add,
178    Sub,
179    Mul,
180    Div,
181    Mod,
182}
183
184/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
185///
186/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
187/// expression-compile time (once), then `eval_compiled()` evaluates it per
188/// element in O(expression-size) with no heap allocation in the hot path.
189#[derive(Clone, Debug)]
190pub(crate) enum CompiledExpr {
191    // ── Leaves ──────────────────────────────────────────────────────────
192    /// A literal value known at compile time.
193    Literal(JValue),
194    /// Explicit `null` literal from `AstNode::Null`.
195    /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
196    /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
197    ExplicitNull,
198    /// Single-level field lookup on the current object: `obj.get("field")`.
199    FieldLookup(String),
200    /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
201    NestedFieldLookup(String, String),
202    /// Variable lookup from enclosing scope (e.g. `$var`).
203    /// Resolved at eval time via a provided variable map.
204    VariableLookup(String),
205
206    // ── Comparison ──────────────────────────────────────────────────────
207    Compare {
208        op: CompiledCmp,
209        lhs: Box<CompiledExpr>,
210        rhs: Box<CompiledExpr>,
211    },
212
213    // ── Arithmetic ──────────────────────────────────────────────────────
214    Arithmetic {
215        op: CompiledArithOp,
216        lhs: Box<CompiledExpr>,
217        rhs: Box<CompiledExpr>,
218    },
219
220    // ── String ──────────────────────────────────────────────────────────
221    Concat(Box<CompiledExpr>, Box<CompiledExpr>),
222
223    // ── Logical ─────────────────────────────────────────────────────────
224    And(Box<CompiledExpr>, Box<CompiledExpr>),
225    Or(Box<CompiledExpr>, Box<CompiledExpr>),
226    Not(Box<CompiledExpr>),
227    /// Negation of a numeric value.
228    Negate(Box<CompiledExpr>),
229
230    // ── Conditional ─────────────────────────────────────────────────────
231    Conditional {
232        condition: Box<CompiledExpr>,
233        then_expr: Box<CompiledExpr>,
234        else_expr: Option<Box<CompiledExpr>>,
235    },
236
237    // ── Compound ────────────────────────────────────────────────────────
238    /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
239    ObjectConstruct(Vec<(String, CompiledExpr)>),
240    /// Array construction: `[expr1, expr2, ...]`
241    ///
242    /// Each element carries a `bool` flag: `true` means the element originated
243    /// from an explicit `AstNode::Array` constructor and must be kept nested even
244    /// if it evaluates to an array. `false` means the element's array value is
245    /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
246    /// Undefined values are always skipped.
247    ArrayConstruct(Vec<(CompiledExpr, bool)>),
248
249    // ── Phase 2 extensions ──────────────────────────────────────────────
250    /// Named variable lookup from context scope (any `$name` not in lambda params).
251    /// Compiled when a named variable is encountered and no allowed_vars list is
252    /// provided (top-level compilation). At runtime, returns the value from the vars
253    /// map (lambda params or captured env), or Undefined if not present.
254    #[allow(dead_code)]
255    ContextVar(String),
256
257    /// Multi-step field path with optional per-step filters: `a.b[pred].c`
258    /// Applies implicit array-mapping semantics at each step.
259    FieldPath(Vec<CompiledStep>),
260
261    /// Call a pure, side-effect-free builtin with compiled arguments.
262    /// Only builtins in COMPILABLE_BUILTINS are allowed here.
263    BuiltinCall {
264        name: &'static str,
265        args: Vec<CompiledExpr>,
266    },
267
268    /// Sequential block: evaluate all expressions, return last value.
269    Block(Vec<CompiledExpr>),
270
271    /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
272    Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
273
274    // ── Higher-order functions with inline lambdas ───────────────────────
275    /// `$map(array, function($v [, $i]) { body })` — compiled when the second
276    /// argument is an inline lambda literal (not a stored variable).
277    /// `params` holds the lambda parameter names (without `$`), 1 or 2 elements.
278    MapCall {
279        array: Box<CompiledExpr>,
280        params: Vec<String>,
281        body: Box<CompiledExpr>,
282    },
283    /// `$filter(array, function($v [, $i]) { body })` — compiled when the second
284    /// argument is an inline lambda literal.
285    FilterCall {
286        array: Box<CompiledExpr>,
287        params: Vec<String>,
288        body: Box<CompiledExpr>,
289    },
290    /// `$reduce(array, function($acc, $v) { body } [, initial])` — compiled when the
291    /// second argument is an inline lambda literal with exactly 2 parameters.
292    ReduceCall {
293        array: Box<CompiledExpr>,
294        params: Vec<String>,
295        body: Box<CompiledExpr>,
296        initial: Option<Box<CompiledExpr>>,
297    },
298}
299
300/// One step in a compiled `FieldPath`.
301#[derive(Clone, Debug)]
302pub(crate) struct CompiledStep {
303    /// Field name to look up at this step.
304    pub field: String,
305    /// Optional predicate filter compiled from a `Stage::Filter` stage.
306    pub filter: Option<CompiledExpr>,
307}
308
309/// Try to compile an AstNode subtree into a CompiledExpr.
310/// Returns None for anything that requires full AST evaluation (lambda calls,
311/// function calls with side effects, complex paths, etc.).
312pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
313    try_compile_expr_inner(node, None)
314}
315
316/// Like `try_compile_expr` but additionally allows the specified variable names
317/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
318/// parameters are known and will be provided via the `vars` map at eval time.
319pub(crate) fn try_compile_expr_with_allowed_vars(
320    node: &AstNode,
321    allowed_vars: &[&str],
322) -> Option<CompiledExpr> {
323    try_compile_expr_inner(node, Some(allowed_vars))
324}
325
326fn try_compile_expr_inner(node: &AstNode, allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
327    match node {
328        // ── Literals ────────────────────────────────────────────────────
329        AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
330        AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
331        AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
332        AstNode::Null => Some(CompiledExpr::ExplicitNull),
333
334        // ── Field access ────────────────────────────────────────────────
335        AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
336
337        // ── Variable lookup ─────────────────────────────────────────────
338        // $ (empty name) always refers to the current element.
339        // Named variables: in HOF mode (allowed_vars=Some), only compile if the
340        // variable is in the allowed set (lambda params supplied via vars map).
341        // In top-level mode (allowed_vars=None), compile unknown variables as
342        // ContextVar — they return Undefined at runtime when no bindings are passed.
343        AstNode::Variable(var) if var.is_empty() => Some(CompiledExpr::VariableLookup(var.clone())),
344        AstNode::Variable(var) => {
345            if let Some(allowed) = allowed_vars {
346                // HOF mode: only compile if the variable is a known lambda param.
347                if allowed.contains(&var.as_str()) {
348                    return Some(CompiledExpr::VariableLookup(var.clone()));
349                }
350            }
351            // Named variables require Context for correct lookup (scope stack, builtins
352            // registry). The compiled fast path passes ctx=None, so fall back to the
353            // tree-walker for all non-empty variable references.
354            None
355        }
356
357        // ── Path expressions ────────────────────────────────────────────
358        AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
359
360        // ── Binary operations ───────────────────────────────────────────
361        AstNode::Binary { op, lhs, rhs } => {
362            let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
363            let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
364            match op {
365                // Comparison
366                BinaryOp::Equal => Some(CompiledExpr::Compare {
367                    op: CompiledCmp::Eq,
368                    lhs: Box::new(compiled_lhs),
369                    rhs: Box::new(compiled_rhs),
370                }),
371                BinaryOp::NotEqual => Some(CompiledExpr::Compare {
372                    op: CompiledCmp::Ne,
373                    lhs: Box::new(compiled_lhs),
374                    rhs: Box::new(compiled_rhs),
375                }),
376                BinaryOp::LessThan => Some(CompiledExpr::Compare {
377                    op: CompiledCmp::Lt,
378                    lhs: Box::new(compiled_lhs),
379                    rhs: Box::new(compiled_rhs),
380                }),
381                BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
382                    op: CompiledCmp::Le,
383                    lhs: Box::new(compiled_lhs),
384                    rhs: Box::new(compiled_rhs),
385                }),
386                BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
387                    op: CompiledCmp::Gt,
388                    lhs: Box::new(compiled_lhs),
389                    rhs: Box::new(compiled_rhs),
390                }),
391                BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
392                    op: CompiledCmp::Ge,
393                    lhs: Box::new(compiled_lhs),
394                    rhs: Box::new(compiled_rhs),
395                }),
396                // Arithmetic
397                BinaryOp::Add => Some(CompiledExpr::Arithmetic {
398                    op: CompiledArithOp::Add,
399                    lhs: Box::new(compiled_lhs),
400                    rhs: Box::new(compiled_rhs),
401                }),
402                BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
403                    op: CompiledArithOp::Sub,
404                    lhs: Box::new(compiled_lhs),
405                    rhs: Box::new(compiled_rhs),
406                }),
407                BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
408                    op: CompiledArithOp::Mul,
409                    lhs: Box::new(compiled_lhs),
410                    rhs: Box::new(compiled_rhs),
411                }),
412                BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
413                    op: CompiledArithOp::Div,
414                    lhs: Box::new(compiled_lhs),
415                    rhs: Box::new(compiled_rhs),
416                }),
417                BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
418                    op: CompiledArithOp::Mod,
419                    lhs: Box::new(compiled_lhs),
420                    rhs: Box::new(compiled_rhs),
421                }),
422                // Logical
423                BinaryOp::And => Some(CompiledExpr::And(
424                    Box::new(compiled_lhs),
425                    Box::new(compiled_rhs),
426                )),
427                BinaryOp::Or => Some(CompiledExpr::Or(
428                    Box::new(compiled_lhs),
429                    Box::new(compiled_rhs),
430                )),
431                // String concat
432                BinaryOp::Concatenate => Some(CompiledExpr::Concat(
433                    Box::new(compiled_lhs),
434                    Box::new(compiled_rhs),
435                )),
436                // Coalesce: return lhs if defined/non-null, else rhs
437                BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
438                    Box::new(compiled_lhs),
439                    Box::new(compiled_rhs),
440                )),
441                // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
442                _ => None,
443            }
444        }
445
446        // ── Unary operations ────────────────────────────────────────────
447        AstNode::Unary { op, operand } => {
448            let compiled = try_compile_expr_inner(operand, allowed_vars)?;
449            match op {
450                crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
451                crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
452            }
453        }
454
455        // ── Conditional ─────────────────────────────────────────────────
456        AstNode::Conditional {
457            condition,
458            then_branch,
459            else_branch,
460        } => {
461            let cond = try_compile_expr_inner(condition, allowed_vars)?;
462            let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
463            let else_e = match else_branch {
464                Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
465                None => None,
466            };
467            Some(CompiledExpr::Conditional {
468                condition: Box::new(cond),
469                then_expr: Box::new(then_e),
470                else_expr: else_e,
471            })
472        }
473
474        // ── Object construction ─────────────────────────────────────────
475        AstNode::Object(pairs) => {
476            let mut fields = Vec::with_capacity(pairs.len());
477            for (key_node, val_node) in pairs {
478                // Key must be a string literal
479                let key = match key_node {
480                    AstNode::String(s) => s.clone(),
481                    _ => return None,
482                };
483                let val = try_compile_expr_inner(val_node, allowed_vars)?;
484                fields.push((key, val));
485            }
486            Some(CompiledExpr::ObjectConstruct(fields))
487        }
488
489        // ── Array construction ──────────────────────────────────────────
490        AstNode::Array(elems) => {
491            let mut compiled = Vec::with_capacity(elems.len());
492            for elem in elems {
493                // Tag whether the element itself is an array constructor: if so, its
494                // array value must be kept nested rather than flattened (tree-walker parity).
495                let is_nested = matches!(elem, AstNode::Array(_));
496                compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
497            }
498            Some(CompiledExpr::ArrayConstruct(compiled))
499        }
500
501        // ── Block (sequential evaluation) ───────────────────────────────
502        AstNode::Block(exprs) if !exprs.is_empty() => {
503            let compiled: Option<Vec<CompiledExpr>> = exprs
504                .iter()
505                .map(|e| try_compile_expr_inner(e, allowed_vars))
506                .collect();
507            compiled.map(CompiledExpr::Block)
508        }
509
510        // ── Pure builtin function calls ──────────────────────────────────
511        AstNode::Function {
512            name,
513            args,
514            is_builtin: true,
515        } => {
516            if is_compilable_builtin(name) {
517                // Arity guard: if the call site passes more args than the builtin accepts,
518                // fall back to the tree-walker so it can raise the correct T0410 error.
519                if let Some(max) = compilable_builtin_max_args(name) {
520                    if args.len() > max {
521                        return None;
522                    }
523                }
524                let compiled_args: Option<Vec<CompiledExpr>> = args
525                    .iter()
526                    .map(|a| try_compile_expr_inner(a, allowed_vars))
527                    .collect();
528                compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
529                    name: static_builtin_name(name),
530                    args: cargs,
531                })
532            } else {
533                try_compile_hof_expr(name, args, allowed_vars)
534            }
535        }
536
537        // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
538        _ => None,
539    }
540}
541
542/// Extract an inline lambda's params and body from an AST node, returning `None` if the
543/// node is not a simple lambda (i.e. has a signature or is a TCO thunk).
544fn extract_inline_lambda(node: &AstNode) -> Option<(&Vec<String>, &AstNode)> {
545    match node {
546        AstNode::Lambda {
547            params,
548            body,
549            signature: None,
550            thunk: false,
551        } => Some((params, body)),
552        _ => None,
553    }
554}
555
556/// Compile the array argument + lambda body for a HOF call, returning `None` if either
557/// fails to compile. The lambda params are added to the allowed-vars set so the body
558/// can reference them.
559fn compile_hof_array_and_body(
560    array_node: &AstNode,
561    params: &[String],
562    body: &AstNode,
563    allowed_vars: Option<&[&str]>,
564) -> Option<(Box<CompiledExpr>, Box<CompiledExpr>)> {
565    let array = try_compile_expr_inner(array_node, allowed_vars)?;
566    let param_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
567    let compiled_body = try_compile_expr_inner(body, Some(&param_refs))?;
568    Some((Box::new(array), Box::new(compiled_body)))
569}
570
571/// Try to compile a higher-order function call (`$map`, `$filter`, `$reduce`) when the
572/// callback argument is an inline lambda literal with a compilable body.
573///
574/// Returns `None` when:
575/// - The callback is not an inline lambda (e.g. a stored variable `$f`) — fall back so
576///   the tree-walker can look up the lambda at runtime.
577/// - The lambda has a signature or is a TCO thunk — semantics require the full evaluator.
578/// - The lambda body is not fully compilable — fall back transparently.
579/// - Param count is outside the supported range (see per-function constraints below).
580fn try_compile_hof_expr(
581    name: &str,
582    args: &[AstNode],
583    allowed_vars: Option<&[&str]>,
584) -> Option<CompiledExpr> {
585    match name {
586        "map" | "filter" => {
587            if args.len() != 2 {
588                return None;
589            }
590            let (params, body) = extract_inline_lambda(&args[1])?;
591            if params.is_empty() || params.len() > 2 {
592                return None;
593            }
594            let (array, compiled_body) =
595                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
596            if name == "map" {
597                Some(CompiledExpr::MapCall {
598                    array,
599                    params: params.clone(),
600                    body: compiled_body,
601                })
602            } else {
603                Some(CompiledExpr::FilterCall {
604                    array,
605                    params: params.clone(),
606                    body: compiled_body,
607                })
608            }
609        }
610        "reduce" => {
611            if args.len() < 2 || args.len() > 3 {
612                return None;
613            }
614            let (params, body) = extract_inline_lambda(&args[1])?;
615            if params.len() != 2 {
616                return None;
617            }
618            let (array, compiled_body) =
619                compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
620            let initial = if args.len() == 3 {
621                Some(Box::new(try_compile_expr_inner(&args[2], allowed_vars)?))
622            } else {
623                None
624            };
625            Some(CompiledExpr::ReduceCall {
626                array,
627                params: params.clone(),
628                body: compiled_body,
629                initial,
630            })
631        }
632        _ => None,
633    }
634}
635
636/// Returns true if the named builtin is pure (no side effects, no context dependency)
637/// and can be safely compiled into a BuiltinCall.
638fn is_compilable_builtin(name: &str) -> bool {
639    matches!(
640        name,
641        "string"
642            | "length"
643            | "substring"
644            | "substringBefore"
645            | "substringAfter"
646            | "uppercase"
647            | "lowercase"
648            | "trim"
649            | "contains"
650            | "split"
651            | "join"
652            | "number"
653            | "floor"
654            | "ceil"
655            | "round"
656            | "abs"
657            | "sqrt"
658            | "sum"
659            | "max"
660            | "min"
661            | "average"
662            | "count"
663            | "boolean"
664            | "not"
665            | "keys"
666            | "append"
667            | "reverse"
668            | "distinct"
669            | "merge"
670    )
671}
672
673/// Maximum number of explicit arguments accepted by each compilable builtin.
674/// Returns `None` for variadic functions with no fixed upper bound.
675/// Used at compile time to fall back to the tree-walker for over-arity calls
676/// (which the tree-walker turns into the correct T0410/T0411 type errors).
677fn compilable_builtin_max_args(name: &str) -> Option<usize> {
678    match name {
679        "string" => Some(2),
680        "length" | "uppercase" | "lowercase" | "trim" => Some(1),
681        "substring" | "split" => Some(3),
682        "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
683        "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
684        "sum" | "max" | "min" | "average" | "count" => Some(1),
685        "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
686        "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
687        _ => None,
688    }
689}
690
691/// Return the `&'static str` for a known compilable builtin name.
692/// SAFETY: only called after `is_compilable_builtin` returns true.
693fn static_builtin_name(name: &str) -> &'static str {
694    match name {
695        "string" => "string",
696        "length" => "length",
697        "substring" => "substring",
698        "substringBefore" => "substringBefore",
699        "substringAfter" => "substringAfter",
700        "uppercase" => "uppercase",
701        "lowercase" => "lowercase",
702        "trim" => "trim",
703        "contains" => "contains",
704        "split" => "split",
705        "join" => "join",
706        "number" => "number",
707        "floor" => "floor",
708        "ceil" => "ceil",
709        "round" => "round",
710        "abs" => "abs",
711        "sqrt" => "sqrt",
712        "sum" => "sum",
713        "max" => "max",
714        "min" => "min",
715        "average" => "average",
716        "count" => "count",
717        "boolean" => "boolean",
718        "not" => "not",
719        "keys" => "keys",
720        "append" => "append",
721        "reverse" => "reverse",
722        "distinct" => "distinct",
723        "merge" => "merge",
724        _ => unreachable!("Not a compilable builtin: {}", name),
725    }
726}
727
728/// Evaluate a compiled expression against a single element.
729///
730/// `data` is the current element (typically an object from an array).
731/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
732///
733/// This is the tight inner loop — no recursion tracking, no scope push/pop,
734/// no AstNode pattern matching.
735#[inline(always)]
736pub(crate) fn eval_compiled(
737    expr: &CompiledExpr,
738    data: &JValue,
739    vars: Option<&HashMap<&str, &JValue>>,
740) -> Result<JValue, EvaluatorError> {
741    eval_compiled_inner(expr, data, vars, None, None)
742}
743
744/// Like `eval_compiled` but with an optional shape cache for O(1) positional
745/// field access. The shape cache maps field names to their index in the object's
746/// internal Vec, enabling `get_index()` instead of hash lookups.
747#[inline(always)]
748fn eval_compiled_shaped(
749    expr: &CompiledExpr,
750    data: &JValue,
751    vars: Option<&HashMap<&str, &JValue>>,
752    shape: &ShapeCache,
753) -> Result<JValue, EvaluatorError> {
754    eval_compiled_inner(expr, data, vars, None, Some(shape))
755}
756
757/// Clone the outer variable bindings into a new HashMap with the given capacity hint.
758/// Used by HOF eval arms to create per-iteration variable scopes that merge outer vars
759/// with lambda parameters.
760#[inline]
761fn clone_outer_vars<'a>(
762    vars: Option<&HashMap<&'a str, &'a JValue>>,
763    capacity: usize,
764) -> HashMap<&'a str, &'a JValue> {
765    vars.map(|v| v.iter().map(|(&k, v)| (k, *v)).collect())
766        .unwrap_or_else(|| HashMap::with_capacity(capacity))
767}
768
769fn eval_compiled_inner(
770    expr: &CompiledExpr,
771    data: &JValue,
772    vars: Option<&HashMap<&str, &JValue>>,
773    ctx: Option<&Context>,
774    shape: Option<&ShapeCache>,
775) -> Result<JValue, EvaluatorError> {
776    match expr {
777        // ── Leaves ──────────────────────────────────────────────────────
778        CompiledExpr::Literal(v) => Ok(v.clone()),
779
780        // ExplicitNull evaluates to Null, but is flagged at compile-time for
781        // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
782        CompiledExpr::ExplicitNull => Ok(JValue::Null),
783
784        CompiledExpr::FieldLookup(field) => match data {
785            JValue::Object(obj) => {
786                // Shape-accelerated: use positional index if available
787                if let Some(shape) = shape {
788                    if let Some(&idx) = shape.get(field.as_str()) {
789                        return Ok(obj
790                            .get_index(idx)
791                            .map(|(_, v)| v.clone())
792                            .unwrap_or(JValue::Undefined));
793                    }
794                }
795                Ok(obj
796                    .get(field.as_str())
797                    .cloned()
798                    .unwrap_or(JValue::Undefined))
799            }
800            _ => Ok(JValue::Undefined),
801        },
802
803        CompiledExpr::NestedFieldLookup(outer, inner) => match data {
804            JValue::Object(obj) => {
805                // Shape-accelerated outer lookup
806                let outer_val = if let Some(shape) = shape {
807                    if let Some(&idx) = shape.get(outer.as_str()) {
808                        obj.get_index(idx).map(|(_, v)| v)
809                    } else {
810                        obj.get(outer.as_str())
811                    }
812                } else {
813                    obj.get(outer.as_str())
814                };
815                Ok(outer_val
816                    .and_then(|v| match v {
817                        JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
818                        _ => None,
819                    })
820                    .unwrap_or(JValue::Undefined))
821            }
822            _ => Ok(JValue::Undefined),
823        },
824
825        CompiledExpr::VariableLookup(var) => {
826            if let Some(vars) = vars {
827                if let Some(val) = vars.get(var.as_str()) {
828                    return Ok((*val).clone());
829                }
830            }
831            // $ (empty var name) refers to the current data
832            if var.is_empty() {
833                return Ok(data.clone());
834            }
835            Ok(JValue::Undefined)
836        }
837
838        // ── Comparison ──────────────────────────────────────────────────
839        CompiledExpr::Compare { op, lhs, rhs } => {
840            let lhs_explicit_null = is_compiled_explicit_null(lhs);
841            let rhs_explicit_null = is_compiled_explicit_null(rhs);
842            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
843            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
844            match op {
845                CompiledCmp::Eq => Ok(JValue::Bool(crate::functions::array::values_equal(
846                    &left, &right,
847                ))),
848                CompiledCmp::Ne => Ok(JValue::Bool(!crate::functions::array::values_equal(
849                    &left, &right,
850                ))),
851                CompiledCmp::Lt => compiled_ordered_cmp(
852                    &left,
853                    &right,
854                    lhs_explicit_null,
855                    rhs_explicit_null,
856                    |a, b| a < b,
857                    |a, b| a < b,
858                ),
859                CompiledCmp::Le => compiled_ordered_cmp(
860                    &left,
861                    &right,
862                    lhs_explicit_null,
863                    rhs_explicit_null,
864                    |a, b| a <= b,
865                    |a, b| a <= b,
866                ),
867                CompiledCmp::Gt => compiled_ordered_cmp(
868                    &left,
869                    &right,
870                    lhs_explicit_null,
871                    rhs_explicit_null,
872                    |a, b| a > b,
873                    |a, b| a > b,
874                ),
875                CompiledCmp::Ge => compiled_ordered_cmp(
876                    &left,
877                    &right,
878                    lhs_explicit_null,
879                    rhs_explicit_null,
880                    |a, b| a >= b,
881                    |a, b| a >= b,
882                ),
883            }
884        }
885
886        // ── Arithmetic ──────────────────────────────────────────────────
887        CompiledExpr::Arithmetic { op, lhs, rhs } => {
888            let lhs_explicit_null = is_compiled_explicit_null(lhs);
889            let rhs_explicit_null = is_compiled_explicit_null(rhs);
890            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
891            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
892            compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
893        }
894
895        // ── String concat ───────────────────────────────────────────────
896        CompiledExpr::Concat(lhs, rhs) => {
897            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
898            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
899            let ls = compiled_to_concat_string(&left)?;
900            let rs = compiled_to_concat_string(&right)?;
901            Ok(JValue::string(format!("{}{}", ls, rs)))
902        }
903
904        // ── Logical ─────────────────────────────────────────────────────
905        CompiledExpr::And(lhs, rhs) => {
906            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
907            if !compiled_is_truthy(&left) {
908                return Ok(JValue::Bool(false));
909            }
910            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
911            Ok(JValue::Bool(compiled_is_truthy(&right)))
912        }
913        CompiledExpr::Or(lhs, rhs) => {
914            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
915            if compiled_is_truthy(&left) {
916                return Ok(JValue::Bool(true));
917            }
918            let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
919            Ok(JValue::Bool(compiled_is_truthy(&right)))
920        }
921        CompiledExpr::Not(inner) => {
922            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
923            Ok(JValue::Bool(!compiled_is_truthy(&val)))
924        }
925        CompiledExpr::Negate(inner) => {
926            let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
927            match val {
928                JValue::Number(n) => Ok(JValue::Number(-n)),
929                JValue::Null => Ok(JValue::Null),
930                // Undefined operand propagates through unary minus, matching the tree-walker.
931                v if v.is_undefined() => Ok(JValue::Undefined),
932                _ => Err(EvaluatorError::TypeError(
933                    "D1002: Cannot negate non-number value".to_string(),
934                )),
935            }
936        }
937
938        // ── Conditional ─────────────────────────────────────────────────
939        CompiledExpr::Conditional {
940            condition,
941            then_expr,
942            else_expr,
943        } => {
944            let cond = eval_compiled_inner(condition, data, vars, ctx, shape)?;
945            if compiled_is_truthy(&cond) {
946                eval_compiled_inner(then_expr, data, vars, ctx, shape)
947            } else if let Some(else_e) = else_expr {
948                eval_compiled_inner(else_e, data, vars, ctx, shape)
949            } else {
950                Ok(JValue::Undefined)
951            }
952        }
953
954        // ── Object construction ─────────────────────────────────────────
955        CompiledExpr::ObjectConstruct(fields) => {
956            let mut result = IndexMap::with_capacity(fields.len());
957            for (key, expr) in fields {
958                let value = eval_compiled_inner(expr, data, vars, ctx, shape)?;
959                if !value.is_undefined() {
960                    result.insert(key.clone(), value);
961                }
962            }
963            Ok(JValue::object(result))
964        }
965
966        // ── Array construction ──────────────────────────────────────────
967        CompiledExpr::ArrayConstruct(elems) => {
968            let mut result = Vec::new();
969            for (elem_expr, is_nested) in elems {
970                let value = eval_compiled_inner(elem_expr, data, vars, ctx, shape)?;
971                // Undefined values are excluded from array constructors (tree-walker parity)
972                if value.is_undefined() {
973                    continue;
974                }
975                if *is_nested {
976                    // Explicit array constructor [...] — keep nested even if it's an array
977                    result.push(value);
978                } else if let JValue::Array(arr) = value {
979                    // Non-constructor that evaluated to an array — flatten one level
980                    result.extend(arr.iter().cloned());
981                } else {
982                    result.push(value);
983                }
984            }
985            Ok(JValue::array(result))
986        }
987
988        // ── Phase 2 new variants ─────────────────────────────────────────
989
990        // ContextVar: named variable lookup from context scope.
991        // In top-level mode (ctx=None, no bindings), returns Undefined.
992        // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
993        // is only ever populated for top-level calls — always Undefined there.
994        CompiledExpr::ContextVar(name) => {
995            // Check vars map first (for lambda params that might shadow context)
996            if let Some(vars) = vars {
997                if let Some(val) = vars.get(name.as_str()) {
998                    return Ok((*val).clone());
999                }
1000            }
1001            // Then check context scope
1002            if let Some(ctx) = ctx {
1003                if let Some(val) = ctx.lookup(name) {
1004                    return Ok(val.clone());
1005                }
1006            }
1007            Ok(JValue::Undefined)
1008        }
1009
1010        // FieldPath: multi-step field access with implicit array mapping.
1011        CompiledExpr::FieldPath(steps) => compiled_eval_field_path(steps, data, vars, ctx, shape),
1012
1013        // BuiltinCall: evaluate all args, dispatch to pure builtin.
1014        CompiledExpr::BuiltinCall { name, args } => {
1015            let mut evaled_args = Vec::with_capacity(args.len());
1016            for arg in args.iter() {
1017                evaled_args.push(eval_compiled_inner(arg, data, vars, ctx, shape)?);
1018            }
1019            call_pure_builtin(name, &evaled_args, data)
1020        }
1021
1022        // Block: evaluate each expression in sequence, return the last value.
1023        CompiledExpr::Block(exprs) => {
1024            let mut result = JValue::Undefined;
1025            for expr in exprs.iter() {
1026                result = eval_compiled_inner(expr, data, vars, ctx, shape)?;
1027            }
1028            Ok(result)
1029        }
1030
1031        // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1032        // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1033        CompiledExpr::Coalesce(lhs, rhs) => {
1034            let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
1035            if left.is_undefined() {
1036                eval_compiled_inner(rhs, data, vars, ctx, shape)
1037            } else {
1038                Ok(left)
1039            }
1040        }
1041
1042        // ── Higher-order functions ─────────────────────────────────────────────
1043        //
1044        // These variants are emitted by try_compile_hof_expr when the HOF argument
1045        // is an inline lambda literal with a compilable body. Outer vars are merged
1046        // with the lambda params so that nested HOF can access variables from
1047        // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1048        CompiledExpr::MapCall {
1049            array,
1050            params,
1051            body,
1052        } => {
1053            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1054            let single_holder;
1055            let items: &[JValue] = match &arr_val {
1056                JValue::Array(a) => a.as_slice(),
1057                JValue::Undefined => return Ok(JValue::Undefined),
1058                other => {
1059                    single_holder = [other.clone()];
1060                    &single_holder[..]
1061                }
1062            };
1063            let mut result = Vec::with_capacity(items.len());
1064            let p0 = params.first().map(|s| s.as_str());
1065
1066            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1067                // 2-param lambda (element + index): build per-iteration because idx_val
1068                // is loop-local and cannot outlive the iteration.
1069                for (idx, item) in items.iter().enumerate() {
1070                    let idx_val = JValue::Number(idx as f64);
1071                    let mut call_vars = clone_outer_vars(vars, 2);
1072                    if let Some(p) = p0 {
1073                        call_vars.insert(p, item);
1074                    }
1075                    call_vars.insert(p1, &idx_val);
1076                    let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1077                    if !mapped.is_undefined() {
1078                        result.push(mapped);
1079                    }
1080                }
1081            } else if let Some(p0) = p0 {
1082                // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1083                let mut call_vars = clone_outer_vars(vars, 1);
1084                for item in items.iter() {
1085                    call_vars.insert(p0, item);
1086                    let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1087                    if !mapped.is_undefined() {
1088                        result.push(mapped);
1089                    }
1090                }
1091            }
1092            Ok(if result.is_empty() {
1093                JValue::Undefined
1094            } else {
1095                JValue::array(result)
1096            })
1097        }
1098
1099        CompiledExpr::FilterCall {
1100            array,
1101            params,
1102            body,
1103        } => {
1104            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1105            if arr_val.is_undefined() || arr_val.is_null() {
1106                return Ok(JValue::Undefined);
1107            }
1108            let single_holder;
1109            let (items, was_single) = match &arr_val {
1110                JValue::Array(a) => (a.as_slice(), false),
1111                other => {
1112                    single_holder = [other.clone()];
1113                    (&single_holder[..], true)
1114                }
1115            };
1116            let mut result = Vec::with_capacity(items.len() / 2);
1117            let p0 = params.first().map(|s| s.as_str());
1118
1119            if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1120                for (idx, item) in items.iter().enumerate() {
1121                    let idx_val = JValue::Number(idx as f64);
1122                    let mut call_vars = clone_outer_vars(vars, 2);
1123                    if let Some(p) = p0 {
1124                        call_vars.insert(p, item);
1125                    }
1126                    call_vars.insert(p1, &idx_val);
1127                    let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1128                    if compiled_is_truthy(&pred) {
1129                        result.push(item.clone());
1130                    }
1131                }
1132            } else if let Some(p0) = p0 {
1133                let mut call_vars = clone_outer_vars(vars, 1);
1134                for item in items.iter() {
1135                    call_vars.insert(p0, item);
1136                    let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1137                    if compiled_is_truthy(&pred) {
1138                        result.push(item.clone());
1139                    }
1140                }
1141            }
1142            if was_single {
1143                Ok(match result.len() {
1144                    0 => JValue::Undefined,
1145                    1 => result.remove(0),
1146                    _ => JValue::array(result),
1147                })
1148            } else {
1149                Ok(JValue::array(result))
1150            }
1151        }
1152
1153        CompiledExpr::ReduceCall {
1154            array,
1155            params,
1156            body,
1157            initial,
1158        } => {
1159            let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1160            let single_holder;
1161            let items: &[JValue] = match &arr_val {
1162                JValue::Array(a) => a.as_slice(),
1163                JValue::Null => return Ok(JValue::Null),
1164                JValue::Undefined => return Ok(JValue::Undefined),
1165                other => {
1166                    single_holder = [other.clone()];
1167                    &single_holder[..]
1168                }
1169            };
1170            let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1171                let init_val = eval_compiled_inner(init_expr, data, vars, ctx, shape)?;
1172                if items.is_empty() {
1173                    return Ok(init_val);
1174                }
1175                (0usize, init_val)
1176            } else {
1177                if items.is_empty() {
1178                    return Ok(JValue::Null);
1179                }
1180                (1, items[0].clone())
1181            };
1182            let acc_param = params[0].as_str();
1183            let item_param = params[1].as_str();
1184            for item in items[start_idx..].iter() {
1185                // Per-iteration HashMap: &accumulator borrow must be released before we
1186                // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1187                let mut call_vars = clone_outer_vars(vars, 2);
1188                call_vars.insert(acc_param, &accumulator);
1189                call_vars.insert(item_param, item);
1190                let new_acc = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1191                drop(call_vars);
1192                accumulator = new_acc;
1193            }
1194            Ok(accumulator)
1195        }
1196    }
1197}
1198
1199/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1200#[inline]
1201pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1202    match value {
1203        JValue::Null | JValue::Undefined => false,
1204        JValue::Bool(b) => *b,
1205        JValue::Number(n) => *n != 0.0,
1206        JValue::String(s) => !s.is_empty(),
1207        JValue::Array(a) => !a.is_empty(),
1208        JValue::Object(o) => !o.is_empty(),
1209        _ => false,
1210    }
1211}
1212
1213/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1214/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1215#[inline]
1216fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1217    matches!(expr, CompiledExpr::ExplicitNull)
1218}
1219
1220/// Ordered comparison for compiled expressions.
1221/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1222#[inline]
1223pub(crate) fn compiled_ordered_cmp(
1224    left: &JValue,
1225    right: &JValue,
1226    left_is_explicit_null: bool,
1227    right_is_explicit_null: bool,
1228    cmp_num: fn(f64, f64) -> bool,
1229    cmp_str: fn(&str, &str) -> bool,
1230) -> Result<JValue, EvaluatorError> {
1231    match (left, right) {
1232        (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1233        (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1234        // Both null/undefined → undefined
1235        (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1236        (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1237        // Explicit null literal with any non-null type → T2010 error
1238        (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1239            "T2010: Type mismatch in comparison".to_string(),
1240        )),
1241        (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1242            "T2010: Type mismatch in comparison".to_string(),
1243        )),
1244        // Boolean with undefined → T2010 error
1245        (JValue::Bool(_), JValue::Null | JValue::Undefined)
1246        | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1247            EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1248        ),
1249        // Number or String with implicit undefined (missing field) → undefined result
1250        (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1251        | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1252            Ok(JValue::Null)
1253        }
1254        // Type mismatch (string vs number)
1255        (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1256            Err(EvaluatorError::EvaluationError(
1257                "T2009: The expressions on either side of operator must be of the same data type"
1258                    .to_string(),
1259            ))
1260        }
1261        _ => Err(EvaluatorError::EvaluationError(
1262            "T2010: Type mismatch in comparison".to_string(),
1263        )),
1264    }
1265}
1266
1267/// Arithmetic for compiled expressions.
1268/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1269#[inline]
1270pub(crate) fn compiled_arithmetic(
1271    op: CompiledArithOp,
1272    left: &JValue,
1273    right: &JValue,
1274    left_is_explicit_null: bool,
1275    right_is_explicit_null: bool,
1276) -> Result<JValue, EvaluatorError> {
1277    let op_sym = match op {
1278        CompiledArithOp::Add => "+",
1279        CompiledArithOp::Sub => "-",
1280        CompiledArithOp::Mul => "*",
1281        CompiledArithOp::Div => "/",
1282        CompiledArithOp::Mod => "%",
1283    };
1284    match (left, right) {
1285        (JValue::Number(a), JValue::Number(b)) => {
1286            let result = match op {
1287                CompiledArithOp::Add => *a + *b,
1288                CompiledArithOp::Sub => *a - *b,
1289                CompiledArithOp::Mul => {
1290                    let r = *a * *b;
1291                    if r.is_infinite() {
1292                        return Err(EvaluatorError::EvaluationError(
1293                            "D1001: Number out of range".to_string(),
1294                        ));
1295                    }
1296                    r
1297                }
1298                CompiledArithOp::Div => {
1299                    if *b == 0.0 {
1300                        return Err(EvaluatorError::EvaluationError(
1301                            "Division by zero".to_string(),
1302                        ));
1303                    }
1304                    *a / *b
1305                }
1306                CompiledArithOp::Mod => {
1307                    if *b == 0.0 {
1308                        return Err(EvaluatorError::EvaluationError(
1309                            "Division by zero".to_string(),
1310                        ));
1311                    }
1312                    *a % *b
1313                }
1314            };
1315            Ok(JValue::Number(result))
1316        }
1317        // Explicit null literal → T2002 error (matching tree-walker behavior)
1318        (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1319            Err(EvaluatorError::TypeError(format!(
1320                "T2002: The left side of the {} operator must evaluate to a number",
1321                op_sym
1322            )))
1323        }
1324        (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1325            Err(EvaluatorError::TypeError(format!(
1326                "T2002: The right side of the {} operator must evaluate to a number",
1327                op_sym
1328            )))
1329        }
1330        // Implicit undefined propagation (from missing field) → undefined result
1331        (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1332            Ok(JValue::Null)
1333        }
1334        _ => Err(EvaluatorError::TypeError(format!(
1335            "Cannot apply {} to {:?} and {:?}",
1336            op_sym, left, right
1337        ))),
1338    }
1339}
1340
1341/// Convert a value to string for concatenation in compiled expressions.
1342#[inline]
1343pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1344    match value {
1345        JValue::String(s) => Ok(s.to_string()),
1346        JValue::Null | JValue::Undefined => Ok(String::new()),
1347        JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1348            match crate::functions::string::string(value, None) {
1349                Ok(JValue::String(s)) => Ok(s.to_string()),
1350                Ok(JValue::Null) => Ok(String::new()),
1351                _ => Err(EvaluatorError::TypeError(
1352                    "Cannot concatenate complex types".to_string(),
1353                )),
1354            }
1355        }
1356        _ => Ok(String::new()),
1357    }
1358}
1359
1360/// Equality comparison for the bytecode VM.
1361#[inline]
1362pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> JValue {
1363    JValue::Bool(crate::functions::array::values_equal(lhs, rhs))
1364}
1365
1366/// String concatenation for the bytecode VM.
1367#[inline]
1368pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1369    let l = compiled_to_concat_string(&lhs)?;
1370    let r = compiled_to_concat_string(&rhs)?;
1371    Ok(JValue::string(l + &r))
1372}
1373
1374/// Entry point for the bytecode VM to call pure builtins by name.
1375#[inline]
1376pub(crate) fn call_pure_builtin_by_name(
1377    name: &str,
1378    args: &[JValue],
1379    data: &JValue,
1380) -> Result<JValue, EvaluatorError> {
1381    call_pure_builtin(name, args, data)
1382}
1383
1384// ──────────────────────────────────────────────────────────────────────────────
1385// Phase 2: path compilation, builtin dispatch, and supporting helpers
1386// ──────────────────────────────────────────────────────────────────────────────
1387
1388/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1389///
1390/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1391/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1392fn try_compile_path(
1393    steps: &[crate::ast::PathStep],
1394    allowed_vars: Option<&[&str]>,
1395) -> Option<CompiledExpr> {
1396    use crate::ast::{AstNode, Stage};
1397
1398    if steps.is_empty() {
1399        return None;
1400    }
1401
1402    // Determine the start of the path:
1403    //   `$.field...`  → starts from current data (drop the leading `$` step)
1404    //   `$var.field`  → variable-prefixed paths: not compiled yet, fall back to tree-walker
1405    //   `field...`    → starts from current data
1406    let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1407        AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1408        AstNode::Variable(_) => return None,
1409        AstNode::Name(_) => steps,
1410        _ => return None,
1411    };
1412
1413    // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1414    // which represent index access in JSONata, not boolean filtering, and the
1415    // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1416    // to stay an array rather than filtering — the tree-walker's
1417    // `evaluate_predicate` special-cases it and the compiled path has no
1418    // equivalent, so bail out rather than silently treating it as `filter(true)`.
1419    let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1420        if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1421            return None;
1422        }
1423        try_compile_expr_inner(node, allowed_vars)
1424    };
1425
1426    // Compile each field step.
1427    // Handles:
1428    //   - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1429    //   - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1430    //     previous step's filter slot, since both encodings have identical runtime semantics.
1431    let mut compiled_steps = Vec::with_capacity(field_steps.len());
1432    for step in field_steps {
1433        // Tuple-stream steps (@ focus / # index / % parent binding) require the
1434        // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1435        // tuple handling). Never compile them to the flat bytecode field path,
1436        // which is unaware of the binding flags and would silently drop them.
1437        if step.focus.is_some()
1438            || step.index_var.is_some()
1439            || step.ancestor_label.is_some()
1440            || step.is_tuple
1441        {
1442            return None;
1443        }
1444        match &step.node {
1445            AstNode::Name(name) => {
1446                let filter = match step.stages.as_slice() {
1447                    [] => None,
1448                    [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1449                    _ => return None,
1450                };
1451                compiled_steps.push(CompiledStep {
1452                    field: name.clone(),
1453                    filter,
1454                });
1455            }
1456            AstNode::Predicate(filter_node) => {
1457                // Standalone predicate step — fold into the previous Name step's filter slot.
1458                if !step.stages.is_empty() {
1459                    return None;
1460                }
1461                let last = compiled_steps.last_mut()?;
1462                if last.filter.is_some() {
1463                    return None;
1464                }
1465                last.filter = Some(compile_filter(filter_node)?);
1466            }
1467            _ => return None,
1468        }
1469    }
1470
1471    if compiled_steps.is_empty() {
1472        // Bare `$` with no further field steps — current-data reference
1473        return Some(CompiledExpr::VariableLookup(String::new()));
1474    }
1475
1476    // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1477    // in HOF mode (allowed_vars=Some), where data is always a single Object element
1478    // from an array. In top-level mode (allowed_vars=None), data can itself be an
1479    // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1480    if allowed_vars.is_some() {
1481        if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1482            return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1483        }
1484        if compiled_steps.len() == 2
1485            && compiled_steps[0].filter.is_none()
1486            && compiled_steps[1].filter.is_none()
1487        {
1488            let outer = compiled_steps.remove(0).field;
1489            let inner = compiled_steps.remove(0).field;
1490            return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1491        }
1492    }
1493
1494    Some(CompiledExpr::FieldPath(compiled_steps))
1495}
1496
1497/// Evaluate a compiled `FieldPath` against `data`.
1498///
1499/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1500/// Filters are applied as predicates: truthy elements are kept.
1501///
1502/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1503/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1504/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1505fn compiled_eval_field_path(
1506    steps: &[CompiledStep],
1507    data: &JValue,
1508    vars: Option<&HashMap<&str, &JValue>>,
1509    ctx: Option<&Context>,
1510    shape: Option<&ShapeCache>,
1511) -> Result<JValue, EvaluatorError> {
1512    let mut current = data.clone();
1513    // Track whether the most recent field step mapped over an array (like the tree-walker's
1514    // `did_array_mapping` flag). Filters also count as array operations.
1515    let mut did_array_mapping = false;
1516    for step in steps {
1517        // Determine if this step will do array mapping before we overwrite `current`
1518        let is_array = matches!(current, JValue::Array(_));
1519        // Field access with implicit array mapping
1520        current = compiled_field_step(&step.field, &current);
1521        if is_array {
1522            did_array_mapping = true;
1523        } else {
1524            // Extracting from a single object resets the flag (tree-walker parity)
1525            did_array_mapping = false;
1526        }
1527        // Apply filter if present (filter is an array operation — keep the flag set)
1528        if let Some(filter) = &step.filter {
1529            current = compiled_apply_filter(filter, &current, vars, ctx, shape)?;
1530            // Filter always implies we operated on an array
1531            did_array_mapping = true;
1532        }
1533    }
1534    // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1535    if did_array_mapping {
1536        Ok(match current {
1537            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1538            other => other,
1539        })
1540    } else {
1541        Ok(current)
1542    }
1543}
1544
1545/// Perform a single-field access with implicit array-mapping semantics.
1546///
1547/// - Object: look up `field`, return its value or Undefined
1548/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1549/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1550/// - Other: Undefined
1551fn compiled_field_step(field: &str, value: &JValue) -> JValue {
1552    match value {
1553        JValue::Object(obj) => {
1554            // Check for tuple: extract from "@" inner object
1555            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1556                if let Some(JValue::Object(inner)) = obj.get("@") {
1557                    return inner.get(field).cloned().unwrap_or(JValue::Undefined);
1558                }
1559                return JValue::Undefined;
1560            }
1561            obj.get(field).cloned().unwrap_or(JValue::Undefined)
1562        }
1563        JValue::Array(arr) => {
1564            // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1565            let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1566                if let JValue::Object(obj) = v {
1567                    if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1568                        return build_shape_cache(v);
1569                    }
1570                }
1571                None
1572            });
1573            let mut result = Vec::new();
1574            for item in arr.iter() {
1575                let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1576                    // Tuple objects need the recursive path for "@" inner lookup.
1577                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1578                        compiled_field_step(field, item)
1579                    } else if let Some(&pos) = sh.get(field) {
1580                        // Positional access with key verification: guards against heterogeneous
1581                        // schemas (objects where the same field is at a different index).
1582                        // On a mismatch, fall back to a regular hash lookup.
1583                        match obj.get_index(pos) {
1584                            Some((k, v)) if k.as_str() == field => v.clone(),
1585                            _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1586                        }
1587                    } else {
1588                        // Field not in the first object's schema — fall back to hash lookup
1589                        // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1590                        obj.get(field).cloned().unwrap_or(JValue::Undefined)
1591                    }
1592                } else {
1593                    compiled_field_step(field, item)
1594                };
1595                match extracted {
1596                    JValue::Undefined => {}
1597                    JValue::Array(inner) => result.extend(inner.iter().cloned()),
1598                    other => result.push(other),
1599                }
1600            }
1601            if result.is_empty() {
1602                JValue::Undefined
1603            } else {
1604                JValue::array(result)
1605            }
1606        }
1607        _ => JValue::Undefined,
1608    }
1609}
1610
1611/// Apply a compiled filter predicate to a value.
1612///
1613/// - Array: return elements for which the predicate is truthy
1614/// - Single value: return it if predicate is truthy, else Undefined
1615/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1616fn compiled_apply_filter(
1617    filter: &CompiledExpr,
1618    value: &JValue,
1619    vars: Option<&HashMap<&str, &JValue>>,
1620    ctx: Option<&Context>,
1621    shape: Option<&ShapeCache>,
1622) -> Result<JValue, EvaluatorError> {
1623    match value {
1624        JValue::Array(arr) => {
1625            let mut result = Vec::new();
1626            // Auto-build shape cache from first element when not provided.
1627            // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1628            let local_shape: Option<ShapeCache> = if shape.is_none() {
1629                arr.first().and_then(build_shape_cache)
1630            } else {
1631                None
1632            };
1633            let effective_shape = shape.or(local_shape.as_ref());
1634            for item in arr.iter() {
1635                let pred = eval_compiled_inner(filter, item, vars, ctx, effective_shape)?;
1636                if compiled_is_truthy(&pred) {
1637                    result.push(item.clone());
1638                }
1639            }
1640            if result.is_empty() {
1641                Ok(JValue::Undefined)
1642            } else if result.len() == 1 {
1643                Ok(result.remove(0))
1644            } else {
1645                Ok(JValue::array(result))
1646            }
1647        }
1648        JValue::Undefined => Ok(JValue::Undefined),
1649        _ => {
1650            let pred = eval_compiled_inner(filter, value, vars, ctx, shape)?;
1651            if compiled_is_truthy(&pred) {
1652                Ok(value.clone())
1653            } else {
1654                Ok(JValue::Undefined)
1655            }
1656        }
1657    }
1658}
1659
1660/// Dispatch a pure builtin function call.
1661///
1662/// Replicates the tree-walker's evaluation for the subset of builtins in
1663/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1664/// `data` is the current context value for implicit-argument insertion.
1665fn call_pure_builtin(name: &str, args: &[JValue], data: &JValue) -> Result<JValue, EvaluatorError> {
1666    use crate::functions;
1667
1668    // Apply implicit context insertion matching the tree-walker
1669    let args_storage: Vec<JValue>;
1670    let effective_args: &[JValue] = if args.is_empty() {
1671        match name {
1672            "string" => {
1673                // $string() with a null/undefined context returns undefined, not "null".
1674                // This mirrors the tree-walker's special case at the function-call site.
1675                if data.is_undefined() || data.is_null() {
1676                    return Ok(JValue::Undefined);
1677                }
1678                args_storage = vec![data.clone()];
1679                &args_storage
1680            }
1681            "number" | "boolean" | "uppercase" | "lowercase" => {
1682                args_storage = vec![data.clone()];
1683                &args_storage
1684            }
1685            _ => args,
1686        }
1687    } else if args.len() == 1 {
1688        match name {
1689            "substringBefore" | "substringAfter" | "contains" | "split" => {
1690                if matches!(data, JValue::String(_)) {
1691                    args_storage = std::iter::once(data.clone())
1692                        .chain(args.iter().cloned())
1693                        .collect();
1694                    &args_storage
1695                } else {
1696                    args
1697                }
1698            }
1699            _ => args,
1700        }
1701    } else {
1702        args
1703    };
1704
1705    // Apply undefined propagation: if the first effective argument is Undefined
1706    // and the function propagates undefined, return Undefined immediately.
1707    // This matches the tree-walker's `propagates_undefined` check.
1708    if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
1709        return Ok(JValue::Undefined);
1710    }
1711
1712    match name {
1713        // ── String functions ────────────────────────────────────────────
1714        "string" => {
1715            // Validate the optional prettify argument: must be a boolean.
1716            let prettify = match effective_args.get(1) {
1717                None => None,
1718                Some(JValue::Bool(b)) => Some(*b),
1719                Some(_) => {
1720                    return Err(EvaluatorError::TypeError(
1721                        "string() prettify parameter must be a boolean".to_string(),
1722                    ))
1723                }
1724            };
1725            let arg = effective_args.first().unwrap_or(&JValue::Null);
1726            Ok(functions::string::string(arg, prettify)?)
1727        }
1728        "length" => match effective_args.first() {
1729            Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1730            // Undefined input propagates (caught above by the undefined-propagation guard).
1731            Some(JValue::Undefined) => Ok(JValue::Undefined),
1732            // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1733            // so the test framework accepts it against any expected T-code).
1734            None => Err(EvaluatorError::EvaluationError(
1735                "length() requires exactly 1 argument".to_string(),
1736            )),
1737            // null and any other non-string type → T0410
1738            _ => Err(EvaluatorError::TypeError(
1739                "T0410: Argument 1 of function length does not match function signature"
1740                    .to_string(),
1741            )),
1742        },
1743        "uppercase" => match effective_args.first() {
1744            Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
1745            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1746            _ => Err(EvaluatorError::TypeError(
1747                "T0410: Argument 1 of function uppercase does not match function signature"
1748                    .to_string(),
1749            )),
1750        },
1751        "lowercase" => match effective_args.first() {
1752            Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
1753            Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1754            _ => Err(EvaluatorError::TypeError(
1755                "T0410: Argument 1 of function lowercase does not match function signature"
1756                    .to_string(),
1757            )),
1758        },
1759        "trim" => match effective_args.first() {
1760            None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
1761            Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
1762            _ => Err(EvaluatorError::TypeError(
1763                "trim() requires a string argument".to_string(),
1764            )),
1765        },
1766        "substring" => {
1767            if effective_args.len() < 2 {
1768                return Err(EvaluatorError::EvaluationError(
1769                    "substring() requires at least 2 arguments".to_string(),
1770                ));
1771            }
1772            match (&effective_args[0], &effective_args[1]) {
1773                (JValue::String(s), JValue::Number(start)) => {
1774                    // Optional 3rd arg (length) must be a number if provided.
1775                    let length = match effective_args.get(2) {
1776                        None => None,
1777                        Some(JValue::Number(l)) => Some(*l as i64),
1778                        Some(_) => {
1779                            return Err(EvaluatorError::TypeError(
1780                                "T0410: Argument 3 of function substring does not match function signature"
1781                                    .to_string(),
1782                            ))
1783                        }
1784                    };
1785                    Ok(functions::string::substring(s, *start as i64, length)?)
1786                }
1787                _ => Err(EvaluatorError::TypeError(
1788                    "T0410: Argument 1 of function substring does not match function signature"
1789                        .to_string(),
1790                )),
1791            }
1792        }
1793        "substringBefore" => {
1794            if effective_args.len() != 2 {
1795                return Err(EvaluatorError::TypeError(
1796                    "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
1797                ));
1798            }
1799            match (&effective_args[0], &effective_args[1]) {
1800                (JValue::String(s), JValue::String(sep)) => {
1801                    Ok(functions::string::substring_before(s, sep)?)
1802                }
1803                // Undefined propagates; null is a type error.
1804                (JValue::Undefined, _) => Ok(JValue::Undefined),
1805                _ => Err(EvaluatorError::TypeError(
1806                    "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
1807                )),
1808            }
1809        }
1810        "substringAfter" => {
1811            if effective_args.len() != 2 {
1812                return Err(EvaluatorError::TypeError(
1813                    "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
1814                ));
1815            }
1816            match (&effective_args[0], &effective_args[1]) {
1817                (JValue::String(s), JValue::String(sep)) => {
1818                    Ok(functions::string::substring_after(s, sep)?)
1819                }
1820                // Undefined propagates; null is a type error.
1821                (JValue::Undefined, _) => Ok(JValue::Undefined),
1822                _ => Err(EvaluatorError::TypeError(
1823                    "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
1824                )),
1825            }
1826        }
1827        "contains" => {
1828            if effective_args.len() != 2 {
1829                return Err(EvaluatorError::EvaluationError(
1830                    "contains() requires exactly 2 arguments".to_string(),
1831                ));
1832            }
1833            match &effective_args[0] {
1834                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1835                JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
1836                _ => Err(EvaluatorError::TypeError(
1837                    "contains() requires a string as the first argument".to_string(),
1838                )),
1839            }
1840        }
1841        "split" => {
1842            if effective_args.len() < 2 {
1843                return Err(EvaluatorError::EvaluationError(
1844                    "split() requires at least 2 arguments".to_string(),
1845                ));
1846            }
1847            match &effective_args[0] {
1848                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1849                JValue::String(s) => {
1850                    // Validate the optional limit argument — must be a positive number.
1851                    let limit = match effective_args.get(2) {
1852                        None => None,
1853                        Some(JValue::Number(n)) => {
1854                            if *n < 0.0 {
1855                                return Err(EvaluatorError::EvaluationError(
1856                                    "D3020: Third argument of split function must be a positive number"
1857                                        .to_string(),
1858                                ));
1859                            }
1860                            Some(n.floor() as usize)
1861                        }
1862                        Some(_) => {
1863                            return Err(EvaluatorError::TypeError(
1864                                "split() limit must be a number".to_string(),
1865                            ))
1866                        }
1867                    };
1868                    Ok(functions::string::split(s, &effective_args[1], limit)?)
1869                }
1870                _ => Err(EvaluatorError::TypeError(
1871                    "split() requires a string as the first argument".to_string(),
1872                )),
1873            }
1874        }
1875        "join" => {
1876            if effective_args.is_empty() {
1877                return Err(EvaluatorError::TypeError(
1878                    "T0410: Argument 1 of function $join does not match function signature"
1879                        .to_string(),
1880                ));
1881            }
1882            match &effective_args[0] {
1883                JValue::Null | JValue::Undefined => Ok(JValue::Null),
1884                // Signature: <a<s>s?:s> — first arg must be an array of strings.
1885                JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
1886                    Err(EvaluatorError::TypeError(
1887                        "T0412: Argument 1 of function $join must be an array of String"
1888                            .to_string(),
1889                    ))
1890                }
1891                JValue::Array(arr) => {
1892                    // All elements must be strings.
1893                    for item in arr.iter() {
1894                        if !matches!(item, JValue::String(_)) {
1895                            return Err(EvaluatorError::TypeError(
1896                                "T0412: Argument 1 of function $join must be an array of String"
1897                                    .to_string(),
1898                            ));
1899                        }
1900                    }
1901                    // Validate separator: must be a string if provided.
1902                    let separator = match effective_args.get(1) {
1903                        None | Some(JValue::Undefined) => None,
1904                        Some(JValue::String(s)) => Some(&**s),
1905                        Some(_) => {
1906                            return Err(EvaluatorError::TypeError(
1907                                "T0410: Argument 2 of function $join does not match function signature (expected String)"
1908                                    .to_string(),
1909                            ))
1910                        }
1911                    };
1912                    Ok(functions::string::join(arr, separator)?)
1913                }
1914                JValue::String(s) => Ok(JValue::String(s.clone())),
1915                _ => Err(EvaluatorError::TypeError(
1916                    "T0412: Argument 1 of function $join must be an array of String".to_string(),
1917                )),
1918            }
1919        }
1920
1921        // ── Numeric functions ───────────────────────────────────────────
1922        "number" => match effective_args.first() {
1923            Some(v) => Ok(functions::numeric::number(v)?),
1924            None => Err(EvaluatorError::EvaluationError(
1925                "number() requires at least 1 argument".to_string(),
1926            )),
1927        },
1928        "floor" => match effective_args.first() {
1929            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1930            Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
1931            _ => Err(EvaluatorError::TypeError(
1932                "floor() requires a number argument".to_string(),
1933            )),
1934        },
1935        "ceil" => match effective_args.first() {
1936            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1937            Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
1938            _ => Err(EvaluatorError::TypeError(
1939                "ceil() requires a number argument".to_string(),
1940            )),
1941        },
1942        "round" => match effective_args.first() {
1943            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1944            Some(JValue::Number(n)) => {
1945                let precision = effective_args.get(1).and_then(|v| {
1946                    if let JValue::Number(p) = v {
1947                        Some(*p as i32)
1948                    } else {
1949                        None
1950                    }
1951                });
1952                Ok(functions::numeric::round(*n, precision)?)
1953            }
1954            _ => Err(EvaluatorError::TypeError(
1955                "round() requires a number argument".to_string(),
1956            )),
1957        },
1958        "abs" => match effective_args.first() {
1959            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1960            Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
1961            _ => Err(EvaluatorError::TypeError(
1962                "abs() requires a number argument".to_string(),
1963            )),
1964        },
1965        "sqrt" => match effective_args.first() {
1966            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1967            Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
1968            _ => Err(EvaluatorError::TypeError(
1969                "sqrt() requires a number argument".to_string(),
1970            )),
1971        },
1972
1973        // ── Aggregation functions ───────────────────────────────────────
1974        "sum" => match effective_args.first() {
1975            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1976            None => Err(EvaluatorError::EvaluationError(
1977                "sum() requires exactly 1 argument".to_string(),
1978            )),
1979            Some(JValue::Null) => Ok(JValue::Null),
1980            Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
1981            Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
1982            Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
1983        },
1984        "max" => match effective_args.first() {
1985            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1986            Some(JValue::Null) | None => Ok(JValue::Null),
1987            Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
1988            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1989            _ => Err(EvaluatorError::TypeError(
1990                "max() requires an array or number argument".to_string(),
1991            )),
1992        },
1993        "min" => match effective_args.first() {
1994            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1995            Some(JValue::Null) | None => Ok(JValue::Null),
1996            Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
1997            Some(v @ JValue::Number(_)) => Ok(v.clone()),
1998            _ => Err(EvaluatorError::TypeError(
1999                "min() requires an array or number argument".to_string(),
2000            )),
2001        },
2002        "average" => match effective_args.first() {
2003            Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2004            Some(JValue::Null) | None => Ok(JValue::Null),
2005            Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2006            Some(v @ JValue::Number(_)) => Ok(v.clone()),
2007            _ => Err(EvaluatorError::TypeError(
2008                "average() requires an array or number argument".to_string(),
2009            )),
2010        },
2011        "count" => match effective_args.first() {
2012            Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2013            Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2014            Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2015            _ => Ok(JValue::from(1i64)),
2016        },
2017
2018        // ── Boolean / logic ─────────────────────────────────────────────
2019        "boolean" => match effective_args.first() {
2020            Some(v) => Ok(functions::boolean::boolean(v)?),
2021            None => Err(EvaluatorError::EvaluationError(
2022                "boolean() requires 1 argument".to_string(),
2023            )),
2024        },
2025        "not" => match effective_args.first() {
2026            Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2027            None => Err(EvaluatorError::EvaluationError(
2028                "not() requires 1 argument".to_string(),
2029            )),
2030        },
2031
2032        // ── Array functions ─────────────────────────────────────────────
2033        "append" => {
2034            if effective_args.len() != 2 {
2035                return Err(EvaluatorError::EvaluationError(
2036                    "append() requires exactly 2 arguments".to_string(),
2037                ));
2038            }
2039            let first = &effective_args[0];
2040            let second = &effective_args[1];
2041            if matches!(second, JValue::Null | JValue::Undefined) {
2042                return Ok(first.clone());
2043            }
2044            if matches!(first, JValue::Null | JValue::Undefined) {
2045                return Ok(second.clone());
2046            }
2047            let arr = match first {
2048                JValue::Array(a) => a.to_vec(),
2049                other => vec![other.clone()],
2050            };
2051            Ok(functions::array::append(&arr, second)?)
2052        }
2053        "reverse" => match effective_args.first() {
2054            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2055            Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2056            _ => Err(EvaluatorError::TypeError(
2057                "reverse() requires an array argument".to_string(),
2058            )),
2059        },
2060        "distinct" => match effective_args.first() {
2061            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2062            Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2063            // Non-array input, and arrays of length <= 1, pass through unchanged
2064            // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2065            Some(other) => Ok(other.clone()),
2066        },
2067
2068        // ── Object functions ────────────────────────────────────────────
2069        "keys" => match effective_args.first() {
2070            Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2071            Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2072            Some(JValue::Object(obj)) => {
2073                if obj.is_empty() {
2074                    Ok(JValue::Null)
2075                } else {
2076                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2077                    if keys.len() == 1 {
2078                        Ok(keys.into_iter().next().unwrap())
2079                    } else {
2080                        Ok(JValue::array(keys))
2081                    }
2082                }
2083            }
2084            Some(JValue::Array(arr)) => {
2085                let mut all_keys: Vec<JValue> = Vec::new();
2086                for item in arr.iter() {
2087                    if let JValue::Object(obj) = item {
2088                        for key in obj.keys() {
2089                            let k = JValue::string(key.clone());
2090                            if !all_keys.contains(&k) {
2091                                all_keys.push(k);
2092                            }
2093                        }
2094                    }
2095                }
2096                if all_keys.is_empty() {
2097                    Ok(JValue::Null)
2098                } else if all_keys.len() == 1 {
2099                    Ok(all_keys.into_iter().next().unwrap())
2100                } else {
2101                    Ok(JValue::array(all_keys))
2102                }
2103            }
2104            _ => Ok(JValue::Null),
2105        },
2106        "merge" => match effective_args.len() {
2107            0 => Err(EvaluatorError::EvaluationError(
2108                "merge() requires at least 1 argument".to_string(),
2109            )),
2110            1 => match &effective_args[0] {
2111                JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2112                JValue::Null | JValue::Undefined => Ok(JValue::Null),
2113                JValue::Object(_) => Ok(effective_args[0].clone()),
2114                _ => Err(EvaluatorError::TypeError(
2115                    "merge() requires objects or an array of objects".to_string(),
2116                )),
2117            },
2118            _ => Ok(functions::object::merge(effective_args)?),
2119        },
2120
2121        _ => unreachable!(
2122            "call_pure_builtin called with non-compilable builtin: {}",
2123            name
2124        ),
2125    }
2126}
2127
2128// ──────────────────────────────────────────────────────────────────────────────
2129// End of CompiledExpr framework
2130// ──────────────────────────────────────────────────────────────────────────────
2131
2132/// Functions that propagate undefined (return undefined when given an undefined argument).
2133/// These functions should return null/undefined when their input path doesn't exist,
2134/// rather than throwing a type error.
2135const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2136    "not",
2137    "boolean",
2138    "length",
2139    "number",
2140    "uppercase",
2141    "lowercase",
2142    "substring",
2143    "substringBefore",
2144    "substringAfter",
2145    "string",
2146];
2147
2148/// Check whether a function propagates undefined values
2149fn propagates_undefined(name: &str) -> bool {
2150    UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2151}
2152
2153/// Iterator-based numeric aggregation helpers.
2154/// These avoid cloning values by iterating over references and extracting f64 values directly.
2155mod aggregation {
2156    use super::*;
2157
2158    /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2159    /// Returns Err if any non-numeric value is encountered.
2160    fn for_each_numeric(
2161        arr: &[JValue],
2162        func_name: &str,
2163        mut f: impl FnMut(f64),
2164    ) -> Result<(), EvaluatorError> {
2165        fn recurse(
2166            arr: &[JValue],
2167            func_name: &str,
2168            f: &mut dyn FnMut(f64),
2169        ) -> Result<(), EvaluatorError> {
2170            for value in arr.iter() {
2171                match value {
2172                    JValue::Array(inner) => recurse(inner, func_name, f)?,
2173                    JValue::Number(n) => {
2174                        f(*n);
2175                    }
2176                    _ => {
2177                        return Err(EvaluatorError::TypeError(format!(
2178                            "{}() requires all array elements to be numbers",
2179                            func_name
2180                        )));
2181                    }
2182                }
2183            }
2184            Ok(())
2185        }
2186        recurse(arr, func_name, &mut f)
2187    }
2188
2189    /// Count elements in a potentially nested array without cloning.
2190    fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2191        let mut count = 0usize;
2192        for_each_numeric(arr, func_name, |_| count += 1)?;
2193        Ok(count)
2194    }
2195
2196    pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2197        if arr.is_empty() {
2198            return Ok(JValue::from(0i64));
2199        }
2200        let mut total = 0.0f64;
2201        for_each_numeric(arr, "sum", |n| total += n)?;
2202        Ok(JValue::Number(total))
2203    }
2204
2205    pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2206        if arr.is_empty() {
2207            return Ok(JValue::Null);
2208        }
2209        let mut max_val = f64::NEG_INFINITY;
2210        for_each_numeric(arr, "max", |n| {
2211            if n > max_val {
2212                max_val = n;
2213            }
2214        })?;
2215        Ok(JValue::Number(max_val))
2216    }
2217
2218    pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2219        if arr.is_empty() {
2220            return Ok(JValue::Null);
2221        }
2222        let mut min_val = f64::INFINITY;
2223        for_each_numeric(arr, "min", |n| {
2224            if n < min_val {
2225                min_val = n;
2226            }
2227        })?;
2228        Ok(JValue::Number(min_val))
2229    }
2230
2231    pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2232        if arr.is_empty() {
2233            return Ok(JValue::Null);
2234        }
2235        let mut total = 0.0f64;
2236        let count = count_numeric(arr, "average")?;
2237        for_each_numeric(arr, "average", |n| total += n)?;
2238        Ok(JValue::Number(total / count as f64))
2239    }
2240}
2241
2242/// Evaluator errors
2243#[derive(Error, Debug)]
2244pub enum EvaluatorError {
2245    #[error("Type error: {0}")]
2246    TypeError(String),
2247
2248    #[error("Reference error: {0}")]
2249    ReferenceError(String),
2250
2251    #[error("Evaluation error: {0}")]
2252    EvaluationError(String),
2253}
2254
2255impl From<crate::functions::FunctionError> for EvaluatorError {
2256    fn from(e: crate::functions::FunctionError) -> Self {
2257        EvaluatorError::EvaluationError(e.to_string())
2258    }
2259}
2260
2261impl From<crate::datetime::DateTimeError> for EvaluatorError {
2262    fn from(e: crate::datetime::DateTimeError) -> Self {
2263        EvaluatorError::EvaluationError(e.to_string())
2264    }
2265}
2266
2267/// Result of evaluating a lambda body that may be a tail call
2268/// Used for trampoline-based tail call optimization
2269enum LambdaResult {
2270    /// Final value - evaluation is complete
2271    JValue(JValue),
2272    /// Tail call - need to continue with another lambda invocation
2273    TailCall {
2274        /// The lambda to call (boxed to reduce enum size)
2275        lambda: Box<StoredLambda>,
2276        /// Arguments for the call
2277        args: Vec<JValue>,
2278        /// Data context for the call
2279        data: JValue,
2280    },
2281}
2282
2283/// Lambda storage
2284/// Stores the AST of a lambda function along with its parameters, optional signature,
2285/// and captured environment for closures
2286#[derive(Clone, Debug)]
2287pub struct StoredLambda {
2288    pub params: Vec<String>,
2289    pub body: AstNode,
2290    /// Pre-compiled body for use in tight inner loops (HOF fast path).
2291    /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2292    pub(crate) compiled_body: Option<CompiledExpr>,
2293    pub signature: Option<String>,
2294    /// Captured environment bindings for closures
2295    pub captured_env: HashMap<String, JValue>,
2296    /// Captured data context for lexical scoping of bare field names
2297    pub captured_data: Option<JValue>,
2298    /// Whether this lambda's body contains tail calls that can be optimized
2299    pub thunk: bool,
2300}
2301
2302/// A single scope in the scope stack
2303struct Scope {
2304    bindings: HashMap<String, JValue>,
2305    lambdas: HashMap<String, StoredLambda>,
2306}
2307
2308impl Scope {
2309    fn new() -> Self {
2310        Scope {
2311            bindings: HashMap::new(),
2312            lambdas: HashMap::new(),
2313        }
2314    }
2315}
2316
2317/// Evaluation context
2318///
2319/// Holds variable bindings and other state needed during evaluation.
2320/// Uses a scope stack for efficient push/pop instead of clone/restore.
2321pub struct Context {
2322    scope_stack: Vec<Scope>,
2323    parent_data: Option<JValue>,
2324}
2325
2326impl Context {
2327    pub fn new() -> Self {
2328        Context {
2329            scope_stack: vec![Scope::new()],
2330            parent_data: None,
2331        }
2332    }
2333
2334    /// Push a new scope onto the stack
2335    fn push_scope(&mut self) {
2336        self.scope_stack.push(Scope::new());
2337    }
2338
2339    /// Pop the top scope from the stack
2340    fn pop_scope(&mut self) {
2341        if self.scope_stack.len() > 1 {
2342            self.scope_stack.pop();
2343        }
2344    }
2345
2346    /// Pop scope but preserve specified lambdas by moving them to the current top scope
2347    fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2348        if self.scope_stack.len() > 1 {
2349            let popped = self.scope_stack.pop().unwrap();
2350            if !lambda_ids.is_empty() {
2351                let top = self.scope_stack.last_mut().unwrap();
2352                for id in lambda_ids {
2353                    if let Some(stored) = popped.lambdas.get(id) {
2354                        top.lambdas.insert(id.clone(), stored.clone());
2355                    }
2356                }
2357            }
2358        }
2359    }
2360
2361    /// Clear all bindings and lambdas in the top scope without deallocating
2362    fn clear_current_scope(&mut self) {
2363        let top = self.scope_stack.last_mut().unwrap();
2364        top.bindings.clear();
2365        top.lambdas.clear();
2366    }
2367
2368    pub fn bind(&mut self, name: String, value: JValue) {
2369        self.scope_stack
2370            .last_mut()
2371            .unwrap()
2372            .bindings
2373            .insert(name, value);
2374    }
2375
2376    pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2377        self.scope_stack
2378            .last_mut()
2379            .unwrap()
2380            .lambdas
2381            .insert(name, lambda);
2382    }
2383
2384    pub fn unbind(&mut self, name: &str) {
2385        // Remove from top scope only
2386        let top = self.scope_stack.last_mut().unwrap();
2387        top.bindings.remove(name);
2388        top.lambdas.remove(name);
2389    }
2390
2391    pub fn lookup(&self, name: &str) -> Option<&JValue> {
2392        // Walk scope stack from top to bottom
2393        for scope in self.scope_stack.iter().rev() {
2394            if let Some(value) = scope.bindings.get(name) {
2395                return Some(value);
2396            }
2397        }
2398        None
2399    }
2400
2401    pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2402        // Walk scope stack from top to bottom
2403        for scope in self.scope_stack.iter().rev() {
2404            if let Some(lambda) = scope.lambdas.get(name) {
2405                return Some(lambda);
2406            }
2407        }
2408        None
2409    }
2410
2411    pub fn set_parent(&mut self, data: JValue) {
2412        self.parent_data = Some(data);
2413    }
2414
2415    pub fn get_parent(&self) -> Option<&JValue> {
2416        self.parent_data.as_ref()
2417    }
2418
2419    /// Collect all bindings across all scopes (for environment capture).
2420    /// Higher scopes shadow lower scopes.
2421    fn all_bindings(&self) -> HashMap<String, JValue> {
2422        let mut result = HashMap::new();
2423        for scope in &self.scope_stack {
2424            for (k, v) in &scope.bindings {
2425                result.insert(k.clone(), v.clone());
2426            }
2427        }
2428        result
2429    }
2430}
2431
2432impl Default for Context {
2433    fn default() -> Self {
2434        Self::new()
2435    }
2436}
2437
2438/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2439/// ...}`) from a value about to leave the evaluator.
2440///
2441/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2442/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2443/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2444/// path step consumes and re-wraps these as evaluation proceeds, but the
2445/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2446/// because the tuple-producing expression itself is the whole result (a bare
2447/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2448/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2449/// wrapper ends up embedded in a field value or array element rather than at
2450/// the top level. This recurses through both array elements and (non-tuple)
2451/// object field values so both shapes are cleaned up, not just a bare
2452/// top-level tuple array.
2453/// Merge a group of tuple wrappers into a single tuple, appending each key's
2454/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2455/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2456/// a key present in one tuple stays a scalar; a key present in several becomes an
2457/// array of the collected values (used by group-by value evaluation so a group
2458/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2459/// N collected focus values).
2460fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2461    fn append(acc: Option<JValue>, v: JValue) -> JValue {
2462        match acc {
2463            None => v,
2464            Some(a) => {
2465                let mut out: Vec<JValue> = match a {
2466                    JValue::Array(arr) => arr.iter().cloned().collect(),
2467                    other => vec![other],
2468                };
2469                match v {
2470                    JValue::Array(arr) => out.extend(arr.iter().cloned()),
2471                    other => out.push(other),
2472                }
2473                JValue::array(out)
2474            }
2475        }
2476    }
2477    let mut result: IndexMap<String, JValue> = IndexMap::new();
2478    for tuple in group {
2479        if let JValue::Object(obj) = tuple {
2480            for (k, v) in obj.iter() {
2481                if k == "__tuple__" {
2482                    result.insert(k.clone(), v.clone());
2483                    continue;
2484                }
2485                let merged = append(result.shift_remove(k), v.clone());
2486                result.insert(k.clone(), merged);
2487            }
2488        }
2489    }
2490    result
2491}
2492
2493fn unwrap_tuple_output(value: JValue) -> JValue {
2494    match value {
2495        JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2496            .get("@")
2497            .cloned()
2498            .map(unwrap_tuple_output)
2499            .unwrap_or(JValue::Undefined),
2500        JValue::Object(obj) => {
2501            let mut new_map = IndexMap::with_capacity(obj.len());
2502            for (k, v) in obj.iter() {
2503                new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2504            }
2505            JValue::object(new_map)
2506        }
2507        JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2508        other => other,
2509    }
2510}
2511
2512/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2513/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2514/// (if anything) was bound under that name beforehand. `restore` puts the
2515/// prior value back -- or removes the binding entirely if there wasn't
2516/// one -- rather than unconditionally unbinding, so a tuple key that
2517/// happens to share a name with a live outer `:=` binding in the same
2518/// scope frame doesn't get permanently deleted once the tuple-row
2519/// evaluation finishes.
2520struct TupleKeyBindings {
2521    saved: Vec<(String, Option<JValue>)>,
2522}
2523
2524impl TupleKeyBindings {
2525    /// True if `name` was one of the keys this guard bound (used by callers
2526    /// that need to know whether a given tuple key is already in scope
2527    /// before binding it a second time under a different role, e.g.
2528    /// `create_tuple_stream`'s ancestor-label handling).
2529    fn contains(&self, name: &str) -> bool {
2530        self.saved.iter().any(|(n, _)| n == name)
2531    }
2532
2533    fn restore(self, evaluator: &mut Evaluator) {
2534        for (name, prior) in self.saved {
2535            match prior {
2536                Some(value) => evaluator.context.bind(name, value),
2537                None => evaluator.context.unbind(&name),
2538            }
2539        }
2540    }
2541}
2542
2543/// Evaluator for JSONata expressions
2544pub struct Evaluator {
2545    context: Context,
2546    recursion_depth: usize,
2547    max_recursion_depth: usize,
2548    /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
2549    /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
2550    /// using the AST node's pointer address (as before) collided whenever the same
2551    /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
2552    /// or other repeated recursion), aliasing unrelated closures that shared an id.
2553    next_lambda_id: u64,
2554    /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
2555    /// wrapper during this top-level `evaluate()` call. Reset at the start of
2556    /// `evaluate()` and checked at the end to decide whether the (recursive,
2557    /// O(result size)) tuple-unwrap pass is needed before returning to the
2558    /// caller — keeps the vast majority of evaluations, which never touch
2559    /// `%`/`@`/`#`, at zero added cost.
2560    tuple_stream_created: bool,
2561    /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
2562    /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
2563    /// by the two consumers that read those carried bindings directly from the
2564    /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
2565    /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
2566    /// evaluating its input path (key/value expressions read `$focus` off the
2567    /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
2568    /// of projecting each tuple's `@`.
2569    keep_tuple_stream: bool,
2570}
2571
2572impl Evaluator {
2573    pub fn new() -> Self {
2574        Evaluator {
2575            context: Context::new(),
2576            recursion_depth: 0,
2577            // Limit recursion depth to prevent stack overflow
2578            // True TCO would allow deeper recursion but requires parser-level thunk marking
2579            max_recursion_depth: 302,
2580            next_lambda_id: 0,
2581            tuple_stream_created: false,
2582            keep_tuple_stream: false,
2583        }
2584    }
2585
2586    pub fn with_context(context: Context) -> Self {
2587        Evaluator {
2588            context,
2589            recursion_depth: 0,
2590            max_recursion_depth: 302,
2591            next_lambda_id: 0,
2592            tuple_stream_created: false,
2593            keep_tuple_stream: false,
2594        }
2595    }
2596
2597    /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
2598    fn fresh_lambda_id(&mut self) -> u64 {
2599        let id = self.next_lambda_id;
2600        self.next_lambda_id += 1;
2601        id
2602    }
2603
2604    /// Invoke a stored lambda with its captured environment and data.
2605    /// This is the standard way to call a StoredLambda, handling the
2606    /// captured_env and captured_data extraction boilerplate.
2607    fn invoke_stored_lambda(
2608        &mut self,
2609        stored: &StoredLambda,
2610        args: &[JValue],
2611        data: &JValue,
2612    ) -> Result<JValue, EvaluatorError> {
2613        // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2614        // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2615        // and no captured lambda/builtin values (those require Context for runtime lookup).
2616        if let Some(ref ce) = stored.compiled_body {
2617            if stored.signature.is_none()
2618                && !stored.thunk
2619                && !stored
2620                    .captured_env
2621                    .values()
2622                    .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2623            {
2624                let call_data = stored.captured_data.as_ref().unwrap_or(data);
2625                let vars: HashMap<&str, &JValue> = stored
2626                    .params
2627                    .iter()
2628                    .zip(args.iter())
2629                    .map(|(p, v)| (p.as_str(), v))
2630                    .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2631                    .collect();
2632                return eval_compiled(ce, call_data, Some(&vars));
2633            }
2634        }
2635
2636        let captured_env = if stored.captured_env.is_empty() {
2637            None
2638        } else {
2639            Some(&stored.captured_env)
2640        };
2641        let captured_data = stored.captured_data.as_ref();
2642        self.invoke_lambda_with_env(
2643            &stored.params,
2644            &stored.body,
2645            stored.signature.as_ref(),
2646            args,
2647            data,
2648            captured_env,
2649            captured_data,
2650            stored.thunk,
2651        )
2652    }
2653
2654    /// Look up a StoredLambda from a JValue that may be a lambda marker.
2655    /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
2656    /// with a valid lambda_id that references a stored lambda.
2657    fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
2658        if let JValue::Lambda { lambda_id, .. } = value {
2659            return self.context.lookup_lambda(lambda_id).cloned();
2660        }
2661        None
2662    }
2663
2664    /// Get the number of parameters a callback function expects by inspecting its AST.
2665    /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
2666    /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
2667    fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
2668        match func_node {
2669            AstNode::Lambda { params, .. } => params.len(),
2670            AstNode::Variable(var_name) => {
2671                // Check if this variable holds a stored lambda
2672                if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
2673                    return stored_lambda.params.len();
2674                }
2675                // Also check if it's a lambda value in bindings (e.g., from partial application)
2676                if let Some(value) = self.context.lookup(var_name) {
2677                    if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
2678                        return stored_lambda.params.len();
2679                    }
2680                }
2681                // Unknown, return max to be safe
2682                usize::MAX
2683            }
2684            AstNode::Function { .. } => {
2685                // For function references, we can't easily determine param count
2686                // Return max to be safe
2687                usize::MAX
2688            }
2689            _ => usize::MAX,
2690        }
2691    }
2692
2693    /// Specialized sort using pre-extracted keys (Schwartzian transform).
2694    /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
2695    /// avoiding O(N log N) hash lookups during comparisons.
2696    fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
2697        if arr.len() <= 1 {
2698            return;
2699        }
2700
2701        // Phase 1: Extract sort keys -- one IndexMap lookup per element
2702        let keys: Vec<SortKey> = arr
2703            .iter()
2704            .map(|item| match item {
2705                JValue::Object(obj) => match obj.get(&spec.field) {
2706                    Some(JValue::Number(n)) => SortKey::Num(*n),
2707                    Some(JValue::String(s)) => SortKey::Str(s.clone()),
2708                    _ => SortKey::None,
2709                },
2710                _ => SortKey::None,
2711            })
2712            .collect();
2713
2714        // Phase 2: Build index permutation sorted by pre-extracted keys
2715        let mut perm: Vec<usize> = (0..arr.len()).collect();
2716        perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
2717
2718        // Phase 3: Apply permutation in-place via cycle-following
2719        let mut placed = vec![false; arr.len()];
2720        for i in 0..arr.len() {
2721            if placed[i] || perm[i] == i {
2722                continue;
2723            }
2724            let mut j = i;
2725            loop {
2726                let target = perm[j];
2727                placed[j] = true;
2728                if target == i {
2729                    break;
2730                }
2731                arr.swap(j, target);
2732                j = target;
2733            }
2734        }
2735    }
2736
2737    /// Merge sort implementation using a comparator function.
2738    /// This replaces the O(n²) bubble sort for better performance on large arrays.
2739    /// The comparator returns true if the first element should come AFTER the second.
2740    fn merge_sort_with_comparator(
2741        &mut self,
2742        arr: &mut [JValue],
2743        comparator: &AstNode,
2744        data: &JValue,
2745    ) -> Result<(), EvaluatorError> {
2746        if arr.len() <= 1 {
2747            return Ok(());
2748        }
2749
2750        // Try specialized fast path for simple field comparisons like
2751        // function($l, $r) { $l.price > $r.price }
2752        if let AstNode::Lambda { params, body, .. } = comparator {
2753            if params.len() >= 2 {
2754                if let Some(spec) = try_specialize_sort_comparator(body, &params[0], &params[1]) {
2755                    Self::merge_sort_specialized(arr, &spec);
2756                    return Ok(());
2757                }
2758            }
2759        }
2760
2761        let mid = arr.len() / 2;
2762
2763        // Sort left half
2764        self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
2765
2766        // Sort right half
2767        self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
2768
2769        // Merge the sorted halves
2770        let mut temp = Vec::with_capacity(arr.len());
2771        let (left, right) = arr.split_at(mid);
2772
2773        let mut i = 0;
2774        let mut j = 0;
2775
2776        // For lambda comparators, use a reusable scope to avoid
2777        // push_scope/pop_scope per comparison (~n log n total comparisons)
2778        if let AstNode::Lambda { params, body, .. } = comparator {
2779            if params.len() >= 2 {
2780                // Pre-clone param names once outside the loop
2781                let param0 = params[0].clone();
2782                let param1 = params[1].clone();
2783                self.context.push_scope();
2784                while i < left.len() && j < right.len() {
2785                    // Reuse scope: clear and rebind instead of push/pop
2786                    self.context.clear_current_scope();
2787                    self.context.bind(param0.clone(), left[i].clone());
2788                    self.context.bind(param1.clone(), right[j].clone());
2789
2790                    let cmp_result = self.evaluate_internal(body, data)?;
2791
2792                    if self.is_truthy(&cmp_result) {
2793                        temp.push(right[j].clone());
2794                        j += 1;
2795                    } else {
2796                        temp.push(left[i].clone());
2797                        i += 1;
2798                    }
2799                }
2800                self.context.pop_scope();
2801            } else {
2802                // Unexpected param count - fall back to generic path
2803                while i < left.len() && j < right.len() {
2804                    let cmp_result = self.apply_function(
2805                        comparator,
2806                        &[left[i].clone(), right[j].clone()],
2807                        data,
2808                    )?;
2809                    if self.is_truthy(&cmp_result) {
2810                        temp.push(right[j].clone());
2811                        j += 1;
2812                    } else {
2813                        temp.push(left[i].clone());
2814                        i += 1;
2815                    }
2816                }
2817            }
2818        } else {
2819            // Non-lambda comparator: use generic apply_function path
2820            while i < left.len() && j < right.len() {
2821                let cmp_result =
2822                    self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
2823                if self.is_truthy(&cmp_result) {
2824                    temp.push(right[j].clone());
2825                    j += 1;
2826                } else {
2827                    temp.push(left[i].clone());
2828                    i += 1;
2829                }
2830            }
2831        }
2832
2833        // Copy remaining elements
2834        temp.extend_from_slice(&left[i..]);
2835        temp.extend_from_slice(&right[j..]);
2836
2837        // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
2838        for (i, val) in temp.into_iter().enumerate() {
2839            arr[i] = val;
2840        }
2841
2842        Ok(())
2843    }
2844
2845    /// Evaluate an AST node against data
2846    ///
2847    /// This is the main entry point for evaluation. It sets up the parent context
2848    /// to be the root data if not already set.
2849    ///
2850    /// Also the single choke point for stripping any lingering tuple-stream
2851    /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
2852    /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
2853    /// tuple-stream representation (see `create_tuple_stream`), and without this
2854    /// a bare (or object/array-nested) tuple-producing expression would leak
2855    /// that internal representation into user-visible output instead of the
2856    /// plain value.
2857    pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
2858        // Set parent context to root data if not already set
2859        if self.context.get_parent().is_none() {
2860            self.context.set_parent(data.clone());
2861        }
2862
2863        self.tuple_stream_created = false;
2864        let result = self.evaluate_internal(node, data)?;
2865        Ok(if self.tuple_stream_created {
2866            unwrap_tuple_output(result)
2867        } else {
2868            result
2869        })
2870    }
2871
2872    /// Fast evaluation for leaf nodes that don't need recursion tracking.
2873    /// Returns Some for literals, simple field access on objects, and simple variable lookups.
2874    /// Returns None for anything requiring the full evaluator.
2875    #[inline(always)]
2876    fn evaluate_leaf(
2877        &mut self,
2878        node: &AstNode,
2879        data: &JValue,
2880    ) -> Option<Result<JValue, EvaluatorError>> {
2881        match node {
2882            AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
2883            AstNode::Number(n) => {
2884                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2885                    Some(Ok(JValue::from(*n as i64)))
2886                } else {
2887                    Some(Ok(JValue::Number(*n)))
2888                }
2889            }
2890            AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
2891            AstNode::Null => Some(Ok(JValue::Null)),
2892            AstNode::Undefined => Some(Ok(JValue::Undefined)),
2893            AstNode::Name(field_name) => match data {
2894                // Array mapping and other cases need full evaluator
2895                JValue::Object(obj) => Some(Ok(obj
2896                    .get(field_name)
2897                    .cloned()
2898                    .unwrap_or(JValue::Undefined))),
2899                _ => None,
2900            },
2901            AstNode::Variable(name) if !name.is_empty() => {
2902                // Simple variable lookup — only fast-path when no tuple data
2903                if let JValue::Object(obj) = data {
2904                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2905                        return None; // Tuple data needs full evaluator
2906                    }
2907                }
2908                // May be a lambda/builtin — needs full evaluator if None
2909                self.context.lookup(name).map(|value| Ok(value.clone()))
2910            }
2911            _ => None,
2912        }
2913    }
2914
2915    /// Internal evaluation method
2916    fn evaluate_internal(
2917        &mut self,
2918        node: &AstNode,
2919        data: &JValue,
2920    ) -> Result<JValue, EvaluatorError> {
2921        // Fast path for leaf nodes — skip recursion tracking overhead
2922        if let Some(result) = self.evaluate_leaf(node, data) {
2923            return result;
2924        }
2925
2926        // Check recursion depth to prevent stack overflow
2927        self.recursion_depth += 1;
2928        if self.recursion_depth > self.max_recursion_depth {
2929            self.recursion_depth -= 1;
2930            return Err(EvaluatorError::EvaluationError(format!(
2931                "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
2932                self.max_recursion_depth
2933            )));
2934        }
2935
2936        // The soft depth counter above is calibrated against a comfortably
2937        // large native stack. Hosts with a much smaller default thread stack
2938        // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
2939        // stack well before this counter trips, crashing the process instead
2940        // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
2941        // transparently swaps in a bigger stack segment when headroom is
2942        // low, so this stays a no-op cost on the common shallow path.
2943        const RED_ZONE: usize = 128 * 1024;
2944        const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
2945        let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
2946            self.evaluate_internal_impl(node, data)
2947        });
2948
2949        self.recursion_depth -= 1;
2950        result
2951    }
2952
2953    /// Internal evaluation implementation (separated to allow depth tracking)
2954    fn evaluate_internal_impl(
2955        &mut self,
2956        node: &AstNode,
2957        data: &JValue,
2958    ) -> Result<JValue, EvaluatorError> {
2959        match node {
2960            AstNode::String(s) => Ok(JValue::string(s.clone())),
2961
2962            // Name nodes represent field access on the current data
2963            AstNode::Name(field_name) => {
2964                match data {
2965                    JValue::Object(obj) => {
2966                        Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
2967                    }
2968                    JValue::Array(arr) => {
2969                        // Map over array
2970                        let mut result = Vec::new();
2971                        for item in arr.iter() {
2972                            if let JValue::Object(obj) = item {
2973                                if let Some(val) = obj.get(field_name) {
2974                                    result.push(val.clone());
2975                                }
2976                            }
2977                        }
2978                        if result.is_empty() {
2979                            Ok(JValue::Undefined)
2980                        } else if result.len() == 1 {
2981                            Ok(result.into_iter().next().unwrap())
2982                        } else {
2983                            Ok(JValue::array(result))
2984                        }
2985                    }
2986                    _ => Ok(JValue::Undefined),
2987                }
2988            }
2989
2990            AstNode::Number(n) => {
2991                // Preserve integer-ness: if the number is a whole number, create an integer JValue
2992                if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2993                    // It's a whole number that can be represented as i64
2994                    Ok(JValue::from(*n as i64))
2995                } else {
2996                    Ok(JValue::Number(*n))
2997                }
2998            }
2999            AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3000            AstNode::Null => Ok(JValue::Null),
3001            AstNode::Undefined => Ok(JValue::Undefined),
3002            AstNode::Placeholder => {
3003                // Placeholders should only appear as function arguments
3004                // If we reach here, it's an error
3005                Err(EvaluatorError::EvaluationError(
3006                    "Placeholder '?' can only be used as a function argument".to_string(),
3007                ))
3008            }
3009            AstNode::Regex { pattern, flags } => {
3010                // Return a regex object as a special JSON value
3011                // This will be recognized by functions like $split, $match, $replace
3012                Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3013            }
3014
3015            AstNode::Variable(name) => {
3016                // Special case: $ alone (empty name) refers to current context
3017                // First check if $ is bound in the context (for closures that captured $)
3018                // Otherwise, use the data parameter
3019                if name.is_empty() {
3020                    if let Some(value) = self.context.lookup("$") {
3021                        return Ok(value.clone());
3022                    }
3023                    // If data is a tuple, return the @ value
3024                    if let JValue::Object(obj) = data {
3025                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3026                            if let Some(inner) = obj.get("@") {
3027                                return Ok(inner.clone());
3028                            }
3029                        }
3030                    }
3031                    return Ok(data.clone());
3032                }
3033
3034                // Check variable bindings FIRST
3035                // This allows function parameters to shadow outer lambdas with the same name
3036                // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3037                if let Some(value) = self.context.lookup(name) {
3038                    return Ok(value.clone());
3039                }
3040
3041                // Check tuple bindings in data (for index binding operator #$var)
3042                // When iterating over a tuple stream, $var can reference the bound index
3043                if let JValue::Object(obj) = data {
3044                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3045                        // Check for the variable in tuple bindings (stored as "$name")
3046                        let binding_key = format!("${}", name);
3047                        if let Some(binding_value) = obj.get(&binding_key) {
3048                            return Ok(binding_value.clone());
3049                        }
3050                    }
3051                }
3052
3053                // Then check if this is a stored lambda (user-defined functions)
3054                if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3055                    // Return a lambda representation that can be passed to higher-order functions
3056                    // Include _lambda_id pointing to the stored lambda so it can be found
3057                    // when captured in closures
3058                    let lambda_repr = JValue::lambda(
3059                        name.as_str(),
3060                        stored_lambda.params.clone(),
3061                        Some(name.to_string()),
3062                        stored_lambda.signature.clone(),
3063                    );
3064                    return Ok(lambda_repr);
3065                }
3066
3067                // Check if this is a built-in function reference (only if not shadowed)
3068                if self.is_builtin_function(name) {
3069                    // Return a marker for built-in functions
3070                    // This allows built-in functions to be passed to higher-order functions
3071                    let builtin_repr = JValue::builtin(name.as_str());
3072                    return Ok(builtin_repr);
3073                }
3074
3075                // Undefined variable - return null (undefined in JSONata semantics)
3076                // This allows expressions like `$not(undefined_var)` to return undefined
3077                // and comparisons like `3 > $undefined` to return undefined
3078                Ok(JValue::Null)
3079            }
3080
3081            AstNode::ParentVariable(name) => {
3082                // Special case: $$ alone (empty name) refers to parent/root context
3083                if name.is_empty() {
3084                    return self.context.get_parent().cloned().ok_or_else(|| {
3085                        EvaluatorError::ReferenceError("Parent context not available".to_string())
3086                    });
3087                }
3088
3089                // For $$name, we need to evaluate name against parent context
3090                // This is similar to $.name but using parent data
3091                let parent_data = self.context.get_parent().ok_or_else(|| {
3092                    EvaluatorError::ReferenceError("Parent context not available".to_string())
3093                })?;
3094
3095                // Access field on parent context
3096                match parent_data {
3097                    JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3098                    _ => Ok(JValue::Null),
3099                }
3100            }
3101
3102            AstNode::Path { steps } => self.evaluate_path(steps, data),
3103
3104            AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3105
3106            AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3107
3108            // Array constructor - JSONata semantics:
3109            AstNode::Array(elements) => {
3110                // - If element is itself an array constructor [...], keep it nested
3111                // - Otherwise, if element evaluates to an array, flatten it
3112                // - Undefined values are excluded
3113                let mut result = Vec::with_capacity(elements.len());
3114                for element in elements {
3115                    // Check if this element is itself an explicit array constructor
3116                    let is_array_constructor = matches!(element, AstNode::Array(_));
3117
3118                    let value = self.evaluate_internal(element, data)?;
3119
3120                    // Skip undefined values in array constructors
3121                    // Note: explicit null is preserved, only undefined (no value) is filtered
3122                    if value.is_undefined() {
3123                        continue;
3124                    }
3125
3126                    if is_array_constructor {
3127                        // Explicit array constructor - keep nested
3128                        result.push(value);
3129                    } else if let JValue::Array(arr) = value {
3130                        // Non-array-constructor that evaluated to array - flatten it
3131                        result.extend(arr.iter().cloned());
3132                    } else {
3133                        // Non-array value - add as-is
3134                        result.push(value);
3135                    }
3136                }
3137                Ok(JValue::array(result))
3138            }
3139
3140            AstNode::Object(pairs) => {
3141                let mut result = IndexMap::with_capacity(pairs.len());
3142
3143                // Check if all keys are string literals — can skip D1009 HashMap
3144                let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3145
3146                if all_literal_keys {
3147                    // Fast path: literal keys, no need for D1009 tracking
3148                    for (key_node, value_node) in pairs.iter() {
3149                        let key = match key_node {
3150                            AstNode::String(s) => s,
3151                            _ => unreachable!(),
3152                        };
3153                        let value = self.evaluate_internal(value_node, data)?;
3154                        if value.is_undefined() {
3155                            continue;
3156                        }
3157                        result.insert(key.clone(), value);
3158                    }
3159                } else {
3160                    let mut key_sources: HashMap<String, usize> = HashMap::new();
3161                    for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3162                        let key = match self.evaluate_internal(key_node, data)? {
3163                            JValue::String(s) => s,
3164                            JValue::Null => continue,
3165                            other => {
3166                                if other.is_undefined() {
3167                                    continue;
3168                                }
3169                                return Err(EvaluatorError::TypeError(format!(
3170                                    "Object key must be a string, got: {:?}",
3171                                    other
3172                                )));
3173                            }
3174                        };
3175
3176                        if let Some(&existing_idx) = key_sources.get(&*key) {
3177                            if existing_idx != pair_index {
3178                                return Err(EvaluatorError::EvaluationError(format!(
3179                                    "D1009: Multiple key expressions evaluate to same key: {}",
3180                                    key
3181                                )));
3182                            }
3183                        }
3184                        key_sources.insert(key.to_string(), pair_index);
3185
3186                        let value = self.evaluate_internal(value_node, data)?;
3187                        if value.is_undefined() {
3188                            continue;
3189                        }
3190                        result.insert(key.to_string(), value);
3191                    }
3192                }
3193                Ok(JValue::object(result))
3194            }
3195
3196            // Object transform: group items by key, then evaluate value once per group
3197            AstNode::ObjectTransform { input, pattern } => {
3198                // Evaluate the input expression. Keep tuple wrappers alive so the
3199                // group-by key/value expressions can read the carried `$focus`
3200                // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3201                let saved_keep = self.keep_tuple_stream;
3202                self.keep_tuple_stream = true;
3203                let input_value = self.evaluate_internal(input, data);
3204                self.keep_tuple_stream = saved_keep;
3205                let input_value = input_value?;
3206
3207                // If input is undefined, return undefined (not empty object)
3208                if input_value.is_undefined() {
3209                    return Ok(JValue::Undefined);
3210                }
3211
3212                // Handle array input - process each item
3213                let items: Vec<JValue> = match input_value {
3214                    JValue::Array(ref arr) => (**arr).clone(),
3215                    JValue::Null => return Ok(JValue::Null),
3216                    other => vec![other],
3217                };
3218
3219                // If array is empty, add undefined to enable literal JSON object generation
3220                let items = if items.is_empty() {
3221                    vec![JValue::Undefined]
3222                } else {
3223                    items
3224                };
3225
3226                // Grouping over a tuple stream ("reduce" mode, mirroring
3227                // jsonata-js evaluateGroupExpression): each item is a
3228                // `{@, $var, !label, __tuple__}` wrapper. The key/value
3229                // expressions are evaluated against the tuple's `@` value with the
3230                // carried focus/index/ancestor keys bound into scope (so
3231                // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3232                // `$e` AND resolves the relative `Phone` against the Contact `@`),
3233                // and grouped tuples are reduced (per-key values appended) before
3234                // the value expression sees them.
3235                let reduce = items.first().is_some_and(|it| {
3236                    matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3237                });
3238
3239                // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3240                // returns the saved prior values so they can be restored.
3241                let bind_tuple = |ev: &mut Self,
3242                                  tuple: &IndexMap<String, JValue>|
3243                 -> Vec<(String, Option<JValue>)> {
3244                    let mut saved = Vec::new();
3245                    for (k, v) in tuple.iter() {
3246                        let name = if let Some(n) = k.strip_prefix('$') {
3247                            if n.is_empty() {
3248                                continue;
3249                            } else {
3250                                n.to_string()
3251                            }
3252                        } else if k.starts_with('!') {
3253                            k.clone()
3254                        } else {
3255                            continue;
3256                        };
3257                        saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3258                        ev.context.bind(name, v.clone());
3259                    }
3260                    saved
3261                };
3262                let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3263                    for (name, old) in saved.into_iter().rev() {
3264                        match old {
3265                            Some(v) => ev.context.bind(name, v),
3266                            None => ev.context.unbind(&name),
3267                        }
3268                    }
3269                };
3270
3271                // Phase 1: Group items by key expression
3272                // groups maps key -> (grouped_data, expr_index)
3273                // When multiple items have same key, their data is appended together
3274                let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3275
3276                // Save the current $ binding to restore later
3277                let saved_dollar = self.context.lookup("$").cloned();
3278
3279                for item in &items {
3280                    // In reduce mode evaluate the key against `@` with tuple keys
3281                    // bound; otherwise against the item itself.
3282                    let (key_data, tuple_saved) = match (reduce, item) {
3283                        (true, JValue::Object(o)) => {
3284                            let saved = bind_tuple(self, o);
3285                            (
3286                                o.get("@").cloned().unwrap_or(JValue::Undefined),
3287                                Some(saved),
3288                            )
3289                        }
3290                        _ => (item.clone(), None),
3291                    };
3292                    self.context.bind("$".to_string(), key_data.clone());
3293
3294                    for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3295                        // Evaluate key with current item as context
3296                        let key = match self.evaluate_internal(key_node, &key_data)? {
3297                            JValue::String(s) => s,
3298                            JValue::Null => continue, // Skip null keys
3299                            other => {
3300                                // Skip undefined keys
3301                                if other.is_undefined() {
3302                                    continue;
3303                                }
3304                                if let Some(saved) = tuple_saved {
3305                                    restore(self, saved);
3306                                }
3307                                return Err(EvaluatorError::TypeError(format!(
3308                                    "T1003: Object key must be a string, got: {:?}",
3309                                    other
3310                                )));
3311                            }
3312                        };
3313
3314                        // Group items by key
3315                        if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3316                            // Key already exists - check if from same expression index
3317                            if *existing_idx != pair_index {
3318                                if let Some(saved) = tuple_saved {
3319                                    restore(self, saved);
3320                                }
3321                                // D1009: multiple key expressions evaluate to same key
3322                                return Err(EvaluatorError::EvaluationError(format!(
3323                                    "D1009: Multiple key expressions evaluate to same key: {}",
3324                                    key
3325                                )));
3326                            }
3327                            // Append item to the group
3328                            existing_data.push(item.clone());
3329                        } else {
3330                            // New key - create new group
3331                            groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3332                        }
3333                    }
3334
3335                    if let Some(saved) = tuple_saved {
3336                        restore(self, saved);
3337                    }
3338                }
3339
3340                // Phase 2: Evaluate value expression for each group
3341                let mut result = IndexMap::new();
3342
3343                for (key, (grouped_data, expr_index)) in groups {
3344                    // Get the value expression for this group
3345                    let (_key_node, value_node) = &pattern[expr_index];
3346
3347                    if reduce {
3348                        // Reduce the grouped tuples into one (per-key values
3349                        // appended), mirroring jsonata-js reduceTupleStream, then
3350                        // evaluate the value against the merged `@` with the merged
3351                        // focus/index/ancestor keys bound.
3352                        let merged = reduce_tuple_stream(&grouped_data);
3353                        let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3354                        let mut tuple_no_at = merged.clone();
3355                        tuple_no_at.shift_remove("@");
3356                        let saved = bind_tuple(self, &tuple_no_at);
3357                        self.context.bind("$".to_string(), context.clone());
3358                        let value = self.evaluate_internal(value_node, &context);
3359                        restore(self, saved);
3360                        let value = value?;
3361                        if !value.is_undefined() {
3362                            result.insert(key, value);
3363                        }
3364                        continue;
3365                    }
3366
3367                    // Determine the context for value evaluation:
3368                    // - If single item, use that item directly
3369                    // - If multiple items, use the array of items
3370                    let context = if grouped_data.len() == 1 {
3371                        grouped_data.into_iter().next().unwrap()
3372                    } else {
3373                        JValue::array(grouped_data)
3374                    };
3375
3376                    // Bind $ to the context for value evaluation
3377                    self.context.bind("$".to_string(), context.clone());
3378
3379                    // Evaluate value expression with grouped context
3380                    let value = self.evaluate_internal(value_node, &context)?;
3381
3382                    // Skip undefined values
3383                    if !value.is_undefined() {
3384                        result.insert(key, value);
3385                    }
3386                }
3387
3388                // Restore the previous $ binding
3389                if let Some(saved) = saved_dollar {
3390                    self.context.bind("$".to_string(), saved);
3391                } else {
3392                    self.context.unbind("$");
3393                }
3394
3395                Ok(JValue::object(result))
3396            }
3397
3398            AstNode::Function {
3399                name,
3400                args,
3401                is_builtin,
3402            } => self.evaluate_function_call(name, args, *is_builtin, data),
3403
3404            // Call: invoke an arbitrary expression as a function
3405            // Used for IIFE patterns like (function($x){...})(5) or chained calls
3406            AstNode::Call { procedure, args } => {
3407                // Evaluate the procedure to get the callable value
3408                let callable = self.evaluate_internal(procedure, data)?;
3409
3410                // Check if it's a lambda value
3411                if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3412                    let mut evaluated_args = Vec::with_capacity(args.len());
3413                    for arg in args.iter() {
3414                        evaluated_args.push(self.evaluate_internal(arg, data)?);
3415                    }
3416                    return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3417                }
3418
3419                // Not a callable value
3420                Err(EvaluatorError::TypeError(format!(
3421                    "Cannot call non-function value: {:?}",
3422                    callable
3423                )))
3424            }
3425
3426            AstNode::Conditional {
3427                condition,
3428                then_branch,
3429                else_branch,
3430            } => {
3431                let condition_value = self.evaluate_internal(condition, data)?;
3432                if self.is_truthy(&condition_value) {
3433                    self.evaluate_internal(then_branch, data)
3434                } else if let Some(else_branch) = else_branch {
3435                    self.evaluate_internal(else_branch, data)
3436                } else {
3437                    // No else branch - return undefined (not null)
3438                    // This allows $map to filter out results from conditionals without else
3439                    Ok(JValue::Undefined)
3440                }
3441            }
3442
3443            AstNode::Block(expressions) => {
3444                // Blocks create a new scope - push scope instead of clone/restore
3445                self.context.push_scope();
3446
3447                let mut result = JValue::Null;
3448                for expr in expressions {
3449                    result = self.evaluate_internal(expr, data)?;
3450                }
3451
3452                // Before popping, preserve any lambdas referenced by the result
3453                // This is essential for closures returned from blocks (IIFE pattern)
3454                let lambdas_to_keep = self.extract_lambda_ids(&result);
3455                self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3456
3457                Ok(result)
3458            }
3459
3460            // Lambda: capture current environment for closure support
3461            AstNode::Lambda {
3462                params,
3463                body,
3464                signature,
3465                thunk,
3466            } => {
3467                let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
3468
3469                let compiled_body = if !thunk {
3470                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3471                    try_compile_expr_with_allowed_vars(body, &var_refs)
3472                } else {
3473                    None
3474                };
3475                let stored_lambda = StoredLambda {
3476                    params: params.clone(),
3477                    body: (**body).clone(),
3478                    compiled_body,
3479                    signature: signature.clone(),
3480                    captured_env: self.capture_environment_for(body, params),
3481                    captured_data: Some(data.clone()),
3482                    thunk: *thunk,
3483                };
3484                self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3485
3486                let lambda_obj = JValue::lambda(
3487                    lambda_id.as_str(),
3488                    params.clone(),
3489                    None::<String>,
3490                    signature.clone(),
3491                );
3492
3493                Ok(lambda_obj)
3494            }
3495
3496            // Wildcard: collect all values from current object
3497            AstNode::Wildcard => {
3498                match data {
3499                    JValue::Object(obj) => {
3500                        let mut result = Vec::new();
3501                        for value in obj.values() {
3502                            // Flatten arrays into the result
3503                            match value {
3504                                JValue::Array(arr) => result.extend(arr.iter().cloned()),
3505                                _ => result.push(value.clone()),
3506                            }
3507                        }
3508                        Ok(JValue::array(result))
3509                    }
3510                    JValue::Array(arr) => {
3511                        // For arrays, wildcard returns all elements
3512                        Ok(JValue::Array(arr.clone()))
3513                    }
3514                    _ => Ok(JValue::Null),
3515                }
3516            }
3517
3518            // Descendant: recursively traverse all nested values
3519            AstNode::Descendant => {
3520                let descendants = self.collect_descendants(data);
3521                if descendants.is_empty() {
3522                    Ok(JValue::Null) // No descendants means undefined
3523                } else {
3524                    Ok(JValue::array(descendants))
3525                }
3526            }
3527
3528            AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3529                "Predicate can only be used in path expressions".to_string(),
3530            )),
3531
3532            // Array grouping: same as Array but prevents flattening in path contexts
3533            AstNode::ArrayGroup(elements) => {
3534                let mut result = Vec::new();
3535                for element in elements {
3536                    let value = self.evaluate_internal(element, data)?;
3537                    result.push(value);
3538                }
3539                Ok(JValue::array(result))
3540            }
3541
3542            AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3543                "Function application can only be used in path expressions".to_string(),
3544            )),
3545
3546            AstNode::Sort { input, terms } => {
3547                // Keep the input path's tuple wrappers so the sort terms can read
3548                // the carried `%`/`$focus`/`$index` bindings per element.
3549                let saved = self.keep_tuple_stream;
3550                self.keep_tuple_stream = true;
3551                let value = self.evaluate_internal(input, data);
3552                self.keep_tuple_stream = saved;
3553                self.evaluate_sort(&value?, terms)
3554            }
3555
3556            // Transform: |location|update[,delete]|
3557            AstNode::Transform {
3558                location,
3559                update,
3560                delete,
3561            } => {
3562                // Check if $ is bound (meaning we're being invoked as a lambda)
3563                if self.context.lookup("$").is_some() {
3564                    // Execute the transformation
3565                    self.execute_transform(location, update, delete.as_deref(), data)
3566                } else {
3567                    // Return a lambda representation
3568                    // The transform will be executed when the lambda is invoked
3569                    let transform_lambda = StoredLambda {
3570                        params: vec!["$".to_string()],
3571                        body: AstNode::Transform {
3572                            location: location.clone(),
3573                            update: update.clone(),
3574                            delete: delete.clone(),
3575                        },
3576                        compiled_body: None, // Transform is not a pure compilable expr
3577                        signature: None,
3578                        captured_env: HashMap::new(),
3579                        captured_data: None, // Transform takes $ as parameter
3580                        thunk: false,
3581                    };
3582
3583                    // Store with a generated unique name
3584                    let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
3585                    self.context.bind_lambda(lambda_name, transform_lambda);
3586
3587                    // Return lambda marker
3588                    Ok(JValue::string("<lambda>"))
3589                }
3590            }
3591
3592            // Parent-reference operator (%): ast_transform has already resolved
3593            // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
3594            // tuple step binds that label into scope (create_tuple_stream +
3595            // needs_tuple_context_binding), so resolving it is an ordinary scope
3596            // lookup, mirroring jsonata-js's
3597            // `case 'parent': result = environment.lookup(expr.slot.label);`.
3598            AstNode::Parent(label) => {
3599                if let Some(v) = self.context.lookup(label) {
3600                    return Ok(v.clone());
3601                }
3602                // Fall back to the tuple wrapper carried as `data`: a `%` used
3603                // inside a predicate/stage over a tuple stream -- e.g.
3604                // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
3605                // predicate is evaluated per tuple with the wrapper as data --
3606                // reads its ancestor from the tuple's `!label` key, which isn't
3607                // separately bound into scope here (mirrors AstNode::Variable's
3608                // tuple-binding fallback below).
3609                if let JValue::Object(obj) = data {
3610                    if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3611                        if let Some(v) = obj.get(label) {
3612                            return Ok(v.clone());
3613                        }
3614                    }
3615                }
3616                Ok(JValue::Undefined)
3617            }
3618        }
3619    }
3620
3621    /// Apply stages (filters/predicates) to a value during field extraction
3622    /// Non-array values are wrapped in an array before filtering (JSONata semantics)
3623    /// This matches the JavaScript reference where stages apply to sequences
3624    fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
3625        // Wrap non-arrays in an array for filtering (JSONata semantics)
3626        let mut result = match value {
3627            JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
3628            JValue::Array(_) => value,
3629            other => JValue::array(vec![other]),
3630        };
3631
3632        for stage in stages {
3633            match stage {
3634                Stage::Filter(predicate_expr) => {
3635                    // When applying stages, use stage-specific predicate logic
3636                    result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
3637                }
3638                // Positional index stages are meaningful only over a tuple stream
3639                // (they set a variable to each tuple's position); they are applied
3640                // in `create_tuple_stream`, not on a plain value sequence here.
3641                Stage::Index(_) => {}
3642            }
3643        }
3644        Ok(result)
3645    }
3646
3647    /// Check if an AST node is definitely a filter expression (comparison/logical)
3648    /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
3649    fn is_filter_predicate(predicate: &AstNode) -> bool {
3650        match predicate {
3651            AstNode::Binary { op, .. } => matches!(
3652                op,
3653                BinaryOp::GreaterThan
3654                    | BinaryOp::GreaterThanOrEqual
3655                    | BinaryOp::LessThan
3656                    | BinaryOp::LessThanOrEqual
3657                    | BinaryOp::Equal
3658                    | BinaryOp::NotEqual
3659                    | BinaryOp::And
3660                    | BinaryOp::Or
3661                    | BinaryOp::In
3662            ),
3663            AstNode::Unary {
3664                op: crate::ast::UnaryOp::Not,
3665                ..
3666            } => true,
3667            _ => false,
3668        }
3669    }
3670
3671    /// Evaluate a predicate as a stage during field extraction
3672    /// This has different semantics than standalone predicates:
3673    /// - Maps index operations over arrays of extracted values
3674    fn evaluate_predicate_as_stage(
3675        &mut self,
3676        current: &JValue,
3677        predicate: &AstNode,
3678    ) -> Result<JValue, EvaluatorError> {
3679        // Special case: empty brackets [] (represented as Boolean(true))
3680        if matches!(predicate, AstNode::Boolean(true)) {
3681            return match current {
3682                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
3683                JValue::Null => Ok(JValue::Null),
3684                other => Ok(JValue::array(vec![other.clone()])),
3685            };
3686        }
3687
3688        match current {
3689            JValue::Array(arr) => {
3690                // For stages: if we have an array of values (from field extraction),
3691                // apply the predicate to each value if appropriate
3692
3693                // Check if predicate is a numeric index
3694                if let AstNode::Number(n) = predicate {
3695                    // Check if this is an array of arrays (extracted array fields)
3696                    let is_array_of_arrays =
3697                        arr.iter().any(|item| matches!(item, JValue::Array(_)));
3698
3699                    if !is_array_of_arrays {
3700                        // Simple values: just index normally
3701                        return self.array_index(current, &JValue::Number(*n));
3702                    }
3703
3704                    // Array of arrays: map index access over each extracted array
3705                    let mut result = Vec::new();
3706                    for item in arr.iter() {
3707                        match item {
3708                            JValue::Array(_) => {
3709                                let indexed = self.array_index(item, &JValue::Number(*n))?;
3710                                if !indexed.is_null() && !indexed.is_undefined() {
3711                                    result.push(indexed);
3712                                }
3713                            }
3714                            _ => {
3715                                if *n == 0.0 {
3716                                    result.push(item.clone());
3717                                }
3718                            }
3719                        }
3720                    }
3721                    return Ok(JValue::array(result));
3722                }
3723
3724                // Short-circuit: if predicate is definitely a comparison/logical expression,
3725                // skip speculative numeric evaluation and go directly to filter logic
3726                if Self::is_filter_predicate(predicate) {
3727                    // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
3728                    if let Some(compiled) = try_compile_expr(predicate) {
3729                        let shape = arr.first().and_then(build_shape_cache);
3730                        let mut filtered = Vec::with_capacity(arr.len());
3731                        for item in arr.iter() {
3732                            let result = if let Some(ref s) = shape {
3733                                eval_compiled_shaped(&compiled, item, None, s)?
3734                            } else {
3735                                eval_compiled(&compiled, item, None)?
3736                            };
3737                            if compiled_is_truthy(&result) {
3738                                filtered.push(item.clone());
3739                            }
3740                        }
3741                        return Ok(JValue::array(filtered));
3742                    }
3743                    // Fallback: full AST evaluation
3744                    let mut filtered = Vec::new();
3745                    for item in arr.iter() {
3746                        let item_result = self.evaluate_internal(predicate, item)?;
3747                        if self.is_truthy(&item_result) {
3748                            filtered.push(item.clone());
3749                        }
3750                    }
3751                    return Ok(JValue::array(filtered));
3752                }
3753
3754                // Try to evaluate the predicate to see if it's a numeric index or array of indices
3755                // If evaluation succeeds and yields a number, use it as an index
3756                // If it yields an array of numbers, use them as multiple indices
3757                // If evaluation fails (e.g., comparison error), treat as filter
3758                match self.evaluate_internal(predicate, current) {
3759                    Ok(JValue::Number(n)) => {
3760                        let n_val = n;
3761                        let is_array_of_arrays =
3762                            arr.iter().any(|item| matches!(item, JValue::Array(_)));
3763
3764                        if !is_array_of_arrays {
3765                            let pred_result = JValue::Number(n_val);
3766                            return self.array_index(current, &pred_result);
3767                        }
3768
3769                        // Array of arrays: map index access
3770                        let mut result = Vec::new();
3771                        let pred_result = JValue::Number(n_val);
3772                        for item in arr.iter() {
3773                            match item {
3774                                JValue::Array(_) => {
3775                                    let indexed = self.array_index(item, &pred_result)?;
3776                                    if !indexed.is_null() && !indexed.is_undefined() {
3777                                        result.push(indexed);
3778                                    }
3779                                }
3780                                _ => {
3781                                    if n_val == 0.0 {
3782                                        result.push(item.clone());
3783                                    }
3784                                }
3785                            }
3786                        }
3787                        return Ok(JValue::array(result));
3788                    }
3789                    Ok(JValue::Array(indices)) => {
3790                        // Array of values - could be indices or filter results
3791                        // Check if all values are numeric
3792                        let has_non_numeric =
3793                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
3794
3795                        if has_non_numeric {
3796                            // Non-numeric values - treat as filter, fall through
3797                        } else {
3798                            // All numeric - use as indices
3799                            let arr_len = arr.len() as i64;
3800                            let mut resolved_indices: Vec<i64> = indices
3801                                .iter()
3802                                .filter_map(|v| {
3803                                    if let JValue::Number(n) = v {
3804                                        let idx = *n as i64;
3805                                        // Resolve negative indices
3806                                        let actual_idx = if idx < 0 { arr_len + idx } else { idx };
3807                                        // Only include valid indices
3808                                        if actual_idx >= 0 && actual_idx < arr_len {
3809                                            Some(actual_idx)
3810                                        } else {
3811                                            None
3812                                        }
3813                                    } else {
3814                                        None
3815                                    }
3816                                })
3817                                .collect();
3818
3819                            // Sort and deduplicate indices
3820                            resolved_indices.sort();
3821                            resolved_indices.dedup();
3822
3823                            // Select elements at each sorted index
3824                            let result: Vec<JValue> = resolved_indices
3825                                .iter()
3826                                .map(|&idx| arr[idx as usize].clone())
3827                                .collect();
3828
3829                            return Ok(JValue::array(result));
3830                        }
3831                    }
3832                    Ok(_) => {
3833                        // Evaluated successfully but not a number or array - might be a filter
3834                        // Fall through to filter logic
3835                    }
3836                    Err(_) => {
3837                        // Evaluation failed - it's likely a filter expression
3838                        // Fall through to filter logic
3839                    }
3840                }
3841
3842                // It's a filter expression
3843                let mut filtered = Vec::new();
3844                for item in arr.iter() {
3845                    let item_result = self.evaluate_internal(predicate, item)?;
3846                    if self.is_truthy(&item_result) {
3847                        filtered.push(item.clone());
3848                    }
3849                }
3850                Ok(JValue::array(filtered))
3851            }
3852            JValue::Null => {
3853                // Null: return null
3854                Ok(JValue::Null)
3855            }
3856            other => {
3857                // Non-array values: treat as single-element conceptual array
3858                // For numeric predicates: index 0 returns the value, other indices return null
3859                // For boolean predicates: if truthy, return value; if falsy, return null
3860
3861                // Check if predicate is a numeric index
3862                if let AstNode::Number(n) = predicate {
3863                    // Index 0 returns the value, other indices return null
3864                    if *n == 0.0 {
3865                        return Ok(other.clone());
3866                    } else {
3867                        return Ok(JValue::Null);
3868                    }
3869                }
3870
3871                // Try to evaluate the predicate to see if it's a numeric index
3872                match self.evaluate_internal(predicate, other) {
3873                    Ok(JValue::Number(n)) => {
3874                        // Index 0 returns the value, other indices return null
3875                        if n == 0.0 {
3876                            Ok(other.clone())
3877                        } else {
3878                            Ok(JValue::Null)
3879                        }
3880                    }
3881                    Ok(pred_result) => {
3882                        // Boolean filter: return value if truthy, null if falsy
3883                        if self.is_truthy(&pred_result) {
3884                            Ok(other.clone())
3885                        } else {
3886                            Ok(JValue::Null)
3887                        }
3888                    }
3889                    Err(e) => Err(e),
3890                }
3891            }
3892        }
3893    }
3894
3895    /// Evaluate a path expression (e.g., foo.bar.baz)
3896    fn evaluate_path(
3897        &mut self,
3898        steps: &[PathStep],
3899        data: &JValue,
3900    ) -> Result<JValue, EvaluatorError> {
3901        // Avoid cloning by using references and only cloning when necessary
3902        if steps.is_empty() {
3903            return Ok(data.clone());
3904        }
3905
3906        // Fast path: single field access on object
3907        // This is a very common pattern, so optimize it.
3908        // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
3909        // creation handled below.
3910        if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
3911            if let AstNode::Name(field_name) = &steps[0].node {
3912                return match data {
3913                    JValue::Object(obj) => {
3914                        // Check if this is a tuple - extract '@' value
3915                        if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3916                            if let Some(JValue::Object(inner)) = obj.get("@") {
3917                                Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
3918                            } else {
3919                                Ok(JValue::Undefined)
3920                            }
3921                        } else {
3922                            Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3923                        }
3924                    }
3925                    JValue::Array(arr) => {
3926                        // Array mapping: extract field from each element
3927                        // Optimized: use references to access fields without cloning entire objects
3928                        // Check first element for tuple-ness (tuples are all-or-nothing)
3929                        let has_tuples = arr.first().is_some_and(|item| {
3930                            matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
3931                        });
3932
3933                        if !has_tuples {
3934                            // Fast path: no tuples, just direct field lookups
3935                            let mut result = Vec::with_capacity(arr.len());
3936                            for item in arr.iter() {
3937                                if let JValue::Object(obj) = item {
3938                                    if let Some(val) = obj.get(field_name) {
3939                                        if !val.is_null() {
3940                                            match val {
3941                                                JValue::Array(arr_val) => {
3942                                                    result.extend(arr_val.iter().cloned());
3943                                                }
3944                                                other => result.push(other.clone()),
3945                                            }
3946                                        }
3947                                    }
3948                                } else if let JValue::Array(inner_arr) = item {
3949                                    let nested_result = self.evaluate_path(
3950                                        &[PathStep::new(AstNode::Name(field_name.clone()))],
3951                                        &JValue::Array(inner_arr.clone()),
3952                                    )?;
3953                                    match nested_result {
3954                                        JValue::Array(nested) => {
3955                                            result.extend(nested.iter().cloned());
3956                                        }
3957                                        JValue::Null => {}
3958                                        other => result.push(other),
3959                                    }
3960                                }
3961                            }
3962
3963                            if result.is_empty() {
3964                                Ok(JValue::Null)
3965                            } else if result.len() == 1 {
3966                                Ok(result.into_iter().next().unwrap())
3967                            } else {
3968                                Ok(JValue::array(result))
3969                            }
3970                        } else {
3971                            // Tuple path: per-element tuple handling
3972                            let mut result = Vec::new();
3973                            for item in arr.iter() {
3974                                match item {
3975                                    JValue::Object(obj) => {
3976                                        let is_tuple =
3977                                            obj.get("__tuple__") == Some(&JValue::Bool(true));
3978
3979                                        if is_tuple {
3980                                            let inner = match obj.get("@") {
3981                                                Some(JValue::Object(inner)) => inner,
3982                                                _ => continue,
3983                                            };
3984
3985                                            if let Some(val) = inner.get(field_name) {
3986                                                if !val.is_null() {
3987                                                    // Build tuple wrapper - only clone bindings when needed
3988                                                    let wrap = |v: JValue| -> JValue {
3989                                                        let mut wrapper = IndexMap::new();
3990                                                        wrapper.insert("@".to_string(), v);
3991                                                        wrapper.insert(
3992                                                            "__tuple__".to_string(),
3993                                                            JValue::Bool(true),
3994                                                        );
3995                                                        for (k, v) in obj.iter() {
3996                                                            if k.starts_with('$') {
3997                                                                wrapper
3998                                                                    .insert(k.clone(), v.clone());
3999                                                            }
4000                                                        }
4001                                                        JValue::object(wrapper)
4002                                                    };
4003
4004                                                    match val {
4005                                                        JValue::Array(arr_val) => {
4006                                                            for item in arr_val.iter() {
4007                                                                result.push(wrap(item.clone()));
4008                                                            }
4009                                                        }
4010                                                        other => result.push(wrap(other.clone())),
4011                                                    }
4012                                                }
4013                                            }
4014                                        } else {
4015                                            // Non-tuple: access field directly by reference, only clone the field value
4016                                            if let Some(val) = obj.get(field_name) {
4017                                                if !val.is_null() {
4018                                                    match val {
4019                                                        JValue::Array(arr_val) => {
4020                                                            for item in arr_val.iter() {
4021                                                                result.push(item.clone());
4022                                                            }
4023                                                        }
4024                                                        other => result.push(other.clone()),
4025                                                    }
4026                                                }
4027                                            }
4028                                        }
4029                                    }
4030                                    JValue::Array(inner_arr) => {
4031                                        // Recursively map over nested array
4032                                        let nested_result = self.evaluate_path(
4033                                            &[PathStep::new(AstNode::Name(field_name.clone()))],
4034                                            &JValue::Array(inner_arr.clone()),
4035                                        )?;
4036                                        // Add nested result to our results
4037                                        match nested_result {
4038                                            JValue::Array(nested) => {
4039                                                // Flatten nested arrays from recursive mapping
4040                                                result.extend(nested.iter().cloned());
4041                                            }
4042                                            JValue::Null => {} // Skip nulls from nested arrays
4043                                            other => result.push(other),
4044                                        }
4045                                    }
4046                                    _ => {} // Skip non-object items
4047                                }
4048                            }
4049
4050                            // Return array result
4051                            // JSONata singleton unwrapping: if we have exactly one result,
4052                            // unwrap it (even if it's an array)
4053                            if result.is_empty() {
4054                                Ok(JValue::Null)
4055                            } else if result.len() == 1 {
4056                                Ok(result.into_iter().next().unwrap())
4057                            } else {
4058                                Ok(JValue::array(result))
4059                            }
4060                        } // end else (tuple path)
4061                    }
4062                    _ => Ok(JValue::Undefined),
4063                };
4064            }
4065        }
4066
4067        // Fast path: 2-step $variable.field with no stages
4068        // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4069        if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4070            if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4071                (&steps[0].node, &steps[1].node)
4072            {
4073                if !var_name.is_empty() {
4074                    if let Some(value) = self.context.lookup(var_name) {
4075                        match value {
4076                            JValue::Object(obj) => {
4077                                return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4078                            }
4079                            JValue::Array(arr) => {
4080                                // Map field extraction over array (same as single-step Name on Array)
4081                                let mut result = Vec::with_capacity(arr.len());
4082                                for item in arr.iter() {
4083                                    if let JValue::Object(obj) = item {
4084                                        if let Some(val) = obj.get(field_name) {
4085                                            if !val.is_null() {
4086                                                match val {
4087                                                    JValue::Array(inner) => {
4088                                                        result.extend(inner.iter().cloned());
4089                                                    }
4090                                                    other => result.push(other.clone()),
4091                                                }
4092                                            }
4093                                        }
4094                                    }
4095                                }
4096                                return match result.len() {
4097                                    0 => Ok(JValue::Null),
4098                                    1 => Ok(result.pop().unwrap()),
4099                                    _ => Ok(JValue::array(result)),
4100                                };
4101                            }
4102                            _ => {} // Fall through to general path evaluation
4103                        }
4104                    }
4105                }
4106            }
4107        }
4108
4109        // Track whether we did array mapping (for singleton unwrapping)
4110        let mut did_array_mapping = false;
4111
4112        // For the first step, work with a reference.
4113        // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4114        // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4115        // first path step where tupleBindings is undefined.
4116        let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4117            JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4118        } else {
4119            match &steps[0].node {
4120                AstNode::Wildcard => {
4121                    // Wildcard as first step
4122                    match data {
4123                        JValue::Object(obj) => {
4124                            let mut result = Vec::new();
4125                            for value in obj.values() {
4126                                // Flatten arrays into the result
4127                                match value {
4128                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4129                                    _ => result.push(value.clone()),
4130                                }
4131                            }
4132                            JValue::array(result)
4133                        }
4134                        JValue::Array(arr) => JValue::Array(arr.clone()),
4135                        _ => JValue::Null,
4136                    }
4137                }
4138                AstNode::Descendant => {
4139                    // Descendant as first step
4140                    let descendants = self.collect_descendants(data);
4141                    JValue::array(descendants)
4142                }
4143                AstNode::ParentVariable(name) => {
4144                    // Parent variable as first step
4145                    let parent_data = self.context.get_parent().ok_or_else(|| {
4146                        EvaluatorError::ReferenceError("Parent context not available".to_string())
4147                    })?;
4148
4149                    if name.is_empty() {
4150                        // $$ alone returns parent context
4151                        parent_data.clone()
4152                    } else {
4153                        // $$field accesses field on parent
4154                        match parent_data {
4155                            JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4156                            _ => JValue::Null,
4157                        }
4158                    }
4159                }
4160                AstNode::Name(field_name) => {
4161                    // Field/property access - get the stages for this step
4162                    let stages = &steps[0].stages;
4163
4164                    match data {
4165                        JValue::Object(obj) => {
4166                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4167                            // Apply any stages to the extracted value
4168                            if !stages.is_empty() {
4169                                self.apply_stages(val, stages)?
4170                            } else {
4171                                val
4172                            }
4173                        }
4174                        JValue::Array(arr) => {
4175                            // Array mapping: extract field from each element and apply stages
4176                            let mut result = Vec::new();
4177                            for item in arr.iter() {
4178                                match item {
4179                                    JValue::Object(obj) => {
4180                                        let val = obj
4181                                            .get(field_name)
4182                                            .cloned()
4183                                            .unwrap_or(JValue::Undefined);
4184                                        if !val.is_null() && !val.is_undefined() {
4185                                            if !stages.is_empty() {
4186                                                // Apply stages to the extracted value
4187                                                let processed_val =
4188                                                    self.apply_stages(val, stages)?;
4189                                                // Stages always return an array (or null); extend results
4190                                                match processed_val {
4191                                                    JValue::Array(arr) => {
4192                                                        result.extend(arr.iter().cloned())
4193                                                    }
4194                                                    JValue::Null => {} // Skip nulls from stage application
4195                                                    other => result.push(other), // Shouldn't happen, but handle it
4196                                                }
4197                                            } else {
4198                                                // No stages: flatten arrays, push scalars
4199                                                match val {
4200                                                    JValue::Array(arr) => {
4201                                                        result.extend(arr.iter().cloned())
4202                                                    }
4203                                                    other => result.push(other),
4204                                                }
4205                                            }
4206                                        }
4207                                    }
4208                                    JValue::Array(inner_arr) => {
4209                                        // Recursively map over nested array
4210                                        let nested_result = self.evaluate_path(
4211                                            &[steps[0].clone()],
4212                                            &JValue::Array(inner_arr.clone()),
4213                                        )?;
4214                                        match nested_result {
4215                                            JValue::Array(nested) => {
4216                                                result.extend(nested.iter().cloned())
4217                                            }
4218                                            JValue::Null => {} // Skip nulls from nested arrays
4219                                            other => result.push(other),
4220                                        }
4221                                    }
4222                                    _ => {} // Skip non-object items
4223                                }
4224                            }
4225                            JValue::array(result)
4226                        }
4227                        JValue::Null => JValue::Null,
4228                        // Accessing field on non-object returns undefined (not an error)
4229                        _ => JValue::Undefined,
4230                    }
4231                }
4232                AstNode::String(string_literal) => {
4233                    // String literal in path context - evaluate as literal and apply stages
4234                    // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4235                    let stages = &steps[0].stages;
4236                    let val = JValue::string(string_literal.clone());
4237
4238                    if !stages.is_empty() {
4239                        // Apply stages (predicates) to the string literal
4240                        let result = self.apply_stages(val, stages)?;
4241                        // Unwrap single-element arrays back to scalar
4242                        // (string literals with predicates should return scalar or null, not arrays)
4243                        match result {
4244                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4245                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4246                            other => other,
4247                        }
4248                    } else {
4249                        val
4250                    }
4251                }
4252                AstNode::Predicate(pred_expr) => {
4253                    // Predicate as first step
4254                    self.evaluate_predicate(data, pred_expr)?
4255                }
4256                _ => {
4257                    // Complex first step - evaluate it. When the step is
4258                    // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4259                    // whose `Product` is `%`-tagged, as in
4260                    // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4261                    // inner path's tuple wrappers so the following predicate/step
4262                    // can read the `!label` bindings.
4263                    let saved_keep = self.keep_tuple_stream;
4264                    if steps[0].is_tuple {
4265                        self.keep_tuple_stream = true;
4266                    }
4267                    let v = self.evaluate_path_step(&steps[0].node, data, data);
4268                    self.keep_tuple_stream = saved_keep;
4269                    v?
4270                }
4271            }
4272        };
4273
4274        // Process remaining steps
4275        for (step_idx, step) in steps[1..].iter().enumerate() {
4276            let is_last_step = step_idx == steps.len() - 2;
4277            // Early return if current is null/undefined - no point continuing
4278            // This handles cases like `blah.{}` where blah doesn't exist
4279            if current.is_null() {
4280                return Ok(JValue::Null);
4281            }
4282            if current.is_undefined() {
4283                return Ok(JValue::Undefined);
4284            }
4285
4286            // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4287            // a tuple stream, which selects a single tuple and unwraps it out of
4288            // the array) must stay a tuple stream so the following step keeps
4289            // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4290            // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4291            if let JValue::Object(o) = &current {
4292                if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4293                    current = JValue::array(vec![current.clone()]);
4294                    // The lone wrapper came from a singleton index selection, so
4295                    // the final result should unwrap back to a scalar (a following
4296                    // object step must not leave a spurious 1-element array).
4297                    did_array_mapping = true;
4298                }
4299            }
4300
4301            // Check if current is a tuple array - if so, we need to bind tuple variables
4302            // to context so they're available in nested expressions (like predicates)
4303            let is_tuple_array = if let JValue::Array(arr) = &current {
4304                arr.first().is_some_and(|first| {
4305                    if let JValue::Object(obj) = first {
4306                        obj.get("__tuple__") == Some(&JValue::Bool(true))
4307                    } else {
4308                        false
4309                    }
4310                })
4311            } else {
4312                false
4313            };
4314
4315            // Tuple-binding step (@ focus / # index / % parent): create/extend the
4316            // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4317            // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4318            // wrappers via the existing tuple-aware handling below.
4319            //
4320            // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
4321            // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
4322            // stream, but ONLY when it is consuming an existing tuple stream:
4323            // its ancestor label lives in those incoming tuples, so
4324            // create_tuple_stream's per-tuple frame binding is what lets
4325            // `evaluate_internal(Parent, ..)` resolve it (and any predicate
4326            // stage on the `%` step then resolves in the same frame). A `%`
4327            // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
4328            // predicate, whose input is plain data, not a tuple stream) must
4329            // NOT be routed here -- it's an ordinary scope lookup.
4330            let is_parent_step_over_tuple =
4331                matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
4332            if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
4333                current = JValue::array(self.create_tuple_stream(step, &current, false)?);
4334                continue;
4335            }
4336
4337            // For tuple arrays with certain step types, we need special handling to bind
4338            // tuple variables to context so they're available in nested expressions.
4339            // This is needed for:
4340            // - Object constructors: {"label": $$.items[$i]} needs $i in context
4341            // - Function applications: .($$.items[$i]) needs $i in context
4342            // - Variable lookups: .$i needs to find the tuple binding
4343            //
4344            // Steps like Name (field access) already have proper tuple handling in their
4345            // specific cases, so we don't intercept those here.
4346            let needs_tuple_context_binding = is_tuple_array
4347                && matches!(
4348                    &step.node,
4349                    AstNode::Object(_)
4350                        | AstNode::FunctionApplication(_)
4351                        | AstNode::Variable(_)
4352                        | AstNode::ArrayGroup(_)
4353                );
4354
4355            if needs_tuple_context_binding {
4356                if let JValue::Array(arr) = &current {
4357                    let mut results = Vec::new();
4358
4359                    for tuple in arr.iter() {
4360                        if let JValue::Object(tuple_obj) = tuple {
4361                            // Extract tuple bindings so nested expressions can see
4362                            // them: `$var` focus/index bindings (stored `$name`,
4363                            // bound as `name`) AND `!label` ancestor bindings for
4364                            // `%` (stored and bound under the full `!label` key).
4365                            // Saves/restores rather than blindly unbinding, so a
4366                            // tuple key that collides with a live outer `:=`
4367                            // binding doesn't get deleted afterward.
4368                            let tuple_bindings = self.bind_tuple_keys(tuple_obj);
4369
4370                            // Get the actual value from the tuple (@ field)
4371                            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4372
4373                            // Evaluate the step
4374                            let step_result = match &step.node {
4375                                AstNode::Variable(_) => {
4376                                    // Variable lookup - check context (which now has bindings)
4377                                    self.evaluate_internal(&step.node, tuple)?
4378                                }
4379                                AstNode::Object(_) | AstNode::ArrayGroup(_) => {
4380                                    // Object / array constructor step (e.g.
4381                                    // `Product.[`Product Name`, %.OrderID]`) -
4382                                    // evaluate on the tuple's `@` value with the
4383                                    // carried `!label`/`$focus` bindings in scope
4384                                    // so an embedded `%` resolves.
4385                                    self.evaluate_internal(&step.node, &actual_data)?
4386                                }
4387                                AstNode::FunctionApplication(inner) => {
4388                                    // A parenthesized step `(expr)` consuming a tuple stream
4389                                    // (e.g. `Account.Order.Product.( %.OrderID )` or
4390                                    // `Employee@$e.(Contact)[...]`): evaluate the INNER
4391                                    // expression on the tuple's `@` value with `$` bound to
4392                                    // it, mirroring the non-tuple FunctionApplication step
4393                                    // handling. Routing the wrapper node itself through
4394                                    // evaluate_internal raises "Function application can only
4395                                    // be used in path expressions".
4396                                    let saved_dollar = self.context.lookup("$").cloned();
4397                                    self.context.bind("$".to_string(), actual_data.clone());
4398                                    // Keep tuple wrappers from the inner path alive:
4399                                    // when `inner` is itself a tuple-carrying path
4400                                    // (e.g. `(Order.Product)` whose `Product` is
4401                                    // `%`-tagged), its `!label` wrappers must survive
4402                                    // to be merged into this tuple by the rewrap below
4403                                    // (they feed a later `%`/`%.%`). Without this the
4404                                    // inner path projects to `@` and drops the labels.
4405                                    let saved_keep = self.keep_tuple_stream;
4406                                    self.keep_tuple_stream = true;
4407                                    let v = self.evaluate_internal(inner, &actual_data);
4408                                    self.keep_tuple_stream = saved_keep;
4409                                    match saved_dollar {
4410                                        Some(s) => self.context.bind("$".to_string(), s),
4411                                        None => self.context.unbind("$"),
4412                                    }
4413                                    v?
4414                                }
4415                                _ => unreachable!(), // We only match specific types above
4416                            };
4417
4418                            // Apply this step's own filter stages (e.g. the
4419                            // `[$substring(title,0,3)='The']` on `.$[...]` in
4420                            // `library.books#$pos.$[...].$pos`) while the tuple
4421                            // bindings are still in scope, so the predicate can
4422                            // reference them and non-matching tuples are dropped.
4423                            let step_result = if step.stages.is_empty() {
4424                                step_result
4425                            } else {
4426                                self.apply_stages(step_result, &step.stages)?
4427                            };
4428
4429                            // Restore previous bindings
4430                            tuple_bindings.restore(self);
4431
4432                            // Rewrap results as tuples carrying this incoming
4433                            // tuple's focus/index/ancestor bindings, so that
4434                            // DOWNSTREAM steps keep seeing them: a predicate like
4435                            // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
4436                            // later `%`/`%.%` in `Account.Order.(Product).{...}`,
4437                            // or a further path step all read those bindings from
4438                            // the tuple wrapper (see AstNode::Variable's tuple
4439                            // fallback). Without rewrapping, the tuple chain is
4440                            // severed after a parenthesized/object/variable step
4441                            // and those references resolve to nothing. The
4442                            // wrappers are projected back to their `@` values by
4443                            // the top-level `unwrap_tuple_output` pass.
4444                            let carried: Vec<(String, JValue)> = tuple_obj
4445                                .iter()
4446                                .filter(|(k, _)| {
4447                                    (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
4448                                })
4449                                .map(|(k, v)| (k.clone(), v.clone()))
4450                                .collect();
4451                            let wrap = |v: JValue| -> JValue {
4452                                match v {
4453                                    // If the step produced a nested tuple stream
4454                                    // (e.g. `(Product)` whose inner `Product` is
4455                                    // itself `%`-tagged), MERGE the inner tuple's
4456                                    // keys over the carried outer bindings, mirroring
4457                                    // jsonata-js's `res.tupleStream` branch
4458                                    // (`Object.assign(tuple, res[bb])`) -- do NOT
4459                                    // double-wrap, which would bury `@`/`!label`
4460                                    // one level down and break a following `%`/`%.%`.
4461                                    JValue::Object(inner)
4462                                        if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
4463                                    {
4464                                        let mut w = IndexMap::new();
4465                                        for (k, val) in &carried {
4466                                            w.insert(k.clone(), val.clone());
4467                                        }
4468                                        for (k, val) in inner.iter() {
4469                                            w.insert(k.clone(), val.clone());
4470                                        }
4471                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4472                                        JValue::object(w)
4473                                    }
4474                                    other => {
4475                                        let mut w = IndexMap::new();
4476                                        w.insert("@".to_string(), other);
4477                                        for (k, val) in &carried {
4478                                            w.insert(k.clone(), val.clone());
4479                                        }
4480                                        w.insert("__tuple__".to_string(), JValue::Bool(true));
4481                                        JValue::object(w)
4482                                    }
4483                                }
4484                            };
4485                            if !step_result.is_null() && !step_result.is_undefined() {
4486                                // Object constructors yield one value per tuple;
4487                                // other steps may yield an array to splice in.
4488                                if matches!(&step.node, AstNode::Object(_)) {
4489                                    results.push(wrap(step_result));
4490                                } else if let JValue::Array(arr) = step_result {
4491                                    for it in arr.iter() {
4492                                        results.push(wrap(it.clone()));
4493                                    }
4494                                } else {
4495                                    results.push(wrap(step_result));
4496                                }
4497                            }
4498                        }
4499                    }
4500
4501                    current = JValue::array(results);
4502                    continue; // Skip the regular step processing
4503                }
4504            }
4505
4506            current = match &step.node {
4507                AstNode::Wildcard => {
4508                    // Wildcard in path
4509                    let stages = &step.stages;
4510                    let wildcard_result = match &current {
4511                        JValue::Object(obj) => {
4512                            let mut result = Vec::new();
4513                            for value in obj.values() {
4514                                // Flatten arrays into the result
4515                                match value {
4516                                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
4517                                    _ => result.push(value.clone()),
4518                                }
4519                            }
4520                            JValue::array(result)
4521                        }
4522                        JValue::Array(arr) => {
4523                            // Map wildcard over array
4524                            let mut all_values = Vec::new();
4525                            for item in arr.iter() {
4526                                match item {
4527                                    JValue::Object(obj) => {
4528                                        for value in obj.values() {
4529                                            // Flatten arrays
4530                                            match value {
4531                                                JValue::Array(arr) => {
4532                                                    all_values.extend(arr.iter().cloned())
4533                                                }
4534                                                _ => all_values.push(value.clone()),
4535                                            }
4536                                        }
4537                                    }
4538                                    JValue::Array(inner) => {
4539                                        all_values.extend(inner.iter().cloned());
4540                                    }
4541                                    _ => {}
4542                                }
4543                            }
4544                            JValue::array(all_values)
4545                        }
4546                        _ => JValue::Null,
4547                    };
4548
4549                    // Apply stages (predicates) if present
4550                    if !stages.is_empty() {
4551                        self.apply_stages(wildcard_result, stages)?
4552                    } else {
4553                        wildcard_result
4554                    }
4555                }
4556                AstNode::Descendant => {
4557                    // Descendant in path
4558                    match &current {
4559                        JValue::Array(arr) => {
4560                            // Collect descendants from all array elements
4561                            let mut all_descendants = Vec::new();
4562                            for item in arr.iter() {
4563                                all_descendants.extend(self.collect_descendants(item));
4564                            }
4565                            JValue::array(all_descendants)
4566                        }
4567                        _ => {
4568                            // Collect descendants from current value
4569                            let descendants = self.collect_descendants(&current);
4570                            JValue::array(descendants)
4571                        }
4572                    }
4573                }
4574                AstNode::Name(field_name) => {
4575                    // Navigate into object field or map over array, applying stages
4576                    let stages = &step.stages;
4577
4578                    match &current {
4579                        JValue::Object(obj) => {
4580                            // Single object field extraction - NOT array mapping
4581                            // This resets did_array_mapping because we're extracting from
4582                            // a single value, not mapping over an array. The field's value
4583                            // (even if it's an array) should be preserved as-is.
4584                            did_array_mapping = false;
4585                            let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4586                            // Apply stages if present
4587                            if !stages.is_empty() {
4588                                self.apply_stages(val, stages)?
4589                            } else {
4590                                val
4591                            }
4592                        }
4593                        JValue::Array(arr) => {
4594                            // Array mapping: extract field from each element and apply stages
4595                            did_array_mapping = true; // Track that we did array mapping
4596
4597                            // Fast path: if no elements are tuples and no stages,
4598                            // skip all tuple checking overhead (common case for products.price etc.)
4599                            // Tuples are all-or-nothing (created by index binding #$i),
4600                            // so checking only the first element is sufficient.
4601                            let has_tuples = arr.first().is_some_and(|item| {
4602                                matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4603                            });
4604
4605                            if !has_tuples && stages.is_empty() {
4606                                let mut result = Vec::with_capacity(arr.len());
4607                                for item in arr.iter() {
4608                                    match item {
4609                                        JValue::Object(obj) => {
4610                                            if let Some(val) = obj.get(field_name) {
4611                                                if !val.is_null() {
4612                                                    match val {
4613                                                        JValue::Array(arr_val) => {
4614                                                            result.extend(arr_val.iter().cloned())
4615                                                        }
4616                                                        other => result.push(other.clone()),
4617                                                    }
4618                                                }
4619                                            }
4620                                        }
4621                                        JValue::Array(_) => {
4622                                            let nested_result =
4623                                                self.evaluate_path(&[step.clone()], item)?;
4624                                            match nested_result {
4625                                                JValue::Array(nested) => {
4626                                                    result.extend(nested.iter().cloned())
4627                                                }
4628                                                JValue::Null => {}
4629                                                other => result.push(other),
4630                                            }
4631                                        }
4632                                        _ => {}
4633                                    }
4634                                }
4635                                JValue::array(result)
4636                            } else {
4637                                // Full path with tuple support and stages
4638                                let mut result = Vec::new();
4639
4640                                for item in arr.iter() {
4641                                    match item {
4642                                        JValue::Object(obj) => {
4643                                            // Check if this is a tuple stream element
4644                                            let (actual_obj, tuple_bindings) = if obj
4645                                                .get("__tuple__")
4646                                                == Some(&JValue::Bool(true))
4647                                            {
4648                                                // This is a tuple - extract '@' value and preserve bindings
4649                                                if let Some(JValue::Object(inner)) = obj.get("@") {
4650                                                    // Collect index bindings (variables starting with $)
4651                                                    let bindings: Vec<(String, JValue)> = obj
4652                                                        .iter()
4653                                                        .filter(|(k, _)| k.starts_with('$'))
4654                                                        .map(|(k, v)| (k.clone(), v.clone()))
4655                                                        .collect();
4656                                                    (inner.clone(), Some(bindings))
4657                                                } else {
4658                                                    continue; // Invalid tuple
4659                                                }
4660                                            } else {
4661                                                (obj.clone(), None)
4662                                            };
4663
4664                                            let val = actual_obj
4665                                                .get(field_name)
4666                                                .cloned()
4667                                                .unwrap_or(JValue::Null);
4668
4669                                            if !val.is_null() {
4670                                                // Helper to wrap value in tuple if we have bindings
4671                                                let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
4672                                                    if let Some(b) = bindings {
4673                                                        let mut wrapper = IndexMap::new();
4674                                                        wrapper.insert("@".to_string(), v);
4675                                                        wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4676                                                        for (k, val) in b {
4677                                                            wrapper.insert(k.clone(), val.clone());
4678                                                        }
4679                                                        JValue::object(wrapper)
4680                                                    } else {
4681                                                        v
4682                                                    }
4683                                                };
4684
4685                                                if !stages.is_empty() {
4686                                                    // Bind this tuple's carried focus/index/ancestor
4687                                                    // bindings so a filter predicate that references
4688                                                    // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
4689                                                    // where the `[$l.isbn=isbn]` stage on the (non-focus)
4690                                                    // `books` step must see `$l` from the enclosing
4691                                                    // `@$l` focus stream. Without this the predicate
4692                                                    // evaluates `$l` as unbound and filters everything out.
4693                                                    let saved_tuple: Vec<(String, Option<JValue>)> =
4694                                                        obj.iter()
4695                                                            .filter_map(|(k, _)| {
4696                                                                if let Some(n) = k.strip_prefix('$')
4697                                                                {
4698                                                                    (!n.is_empty())
4699                                                                        .then(|| n.to_string())
4700                                                                } else if k.starts_with('!') {
4701                                                                    Some(k.clone())
4702                                                                } else {
4703                                                                    None
4704                                                                }
4705                                                            })
4706                                                            .map(|n| {
4707                                                                (
4708                                                                    n.clone(),
4709                                                                    self.context
4710                                                                        .lookup(&n)
4711                                                                        .cloned(),
4712                                                                )
4713                                                            })
4714                                                            .collect();
4715                                                    for (k, v) in obj.iter() {
4716                                                        if let Some(n) = k.strip_prefix('$') {
4717                                                            if !n.is_empty() {
4718                                                                self.context
4719                                                                    .bind(n.to_string(), v.clone());
4720                                                            }
4721                                                        } else if k.starts_with('!') {
4722                                                            self.context.bind(k.clone(), v.clone());
4723                                                        }
4724                                                    }
4725                                                    // Apply stages to the extracted value
4726                                                    let processed_val =
4727                                                        self.apply_stages(val, stages);
4728                                                    for (n, old) in saved_tuple.into_iter().rev() {
4729                                                        match old {
4730                                                            Some(v) => self.context.bind(n, v),
4731                                                            None => self.context.unbind(&n),
4732                                                        }
4733                                                    }
4734                                                    let processed_val = processed_val?;
4735                                                    // Stages always return an array (or null); extend results
4736                                                    match processed_val {
4737                                                        JValue::Array(arr) => {
4738                                                            for item in arr.iter() {
4739                                                                result.push(wrap_in_tuple(
4740                                                                    item.clone(),
4741                                                                    &tuple_bindings,
4742                                                                ));
4743                                                            }
4744                                                        }
4745                                                        JValue::Null => {} // Skip nulls from stage application
4746                                                        other => result.push(wrap_in_tuple(
4747                                                            other,
4748                                                            &tuple_bindings,
4749                                                        )),
4750                                                    }
4751                                                } else {
4752                                                    // No stages: flatten arrays, push scalars
4753                                                    // But preserve tuple bindings!
4754                                                    match val {
4755                                                        JValue::Array(arr) => {
4756                                                            for item in arr.iter() {
4757                                                                result.push(wrap_in_tuple(
4758                                                                    item.clone(),
4759                                                                    &tuple_bindings,
4760                                                                ));
4761                                                            }
4762                                                        }
4763                                                        other => result.push(wrap_in_tuple(
4764                                                            other,
4765                                                            &tuple_bindings,
4766                                                        )),
4767                                                    }
4768                                                }
4769                                            }
4770                                        }
4771                                        JValue::Array(_) => {
4772                                            // Recursively map over nested array
4773                                            let nested_result =
4774                                                self.evaluate_path(&[step.clone()], item)?;
4775                                            match nested_result {
4776                                                JValue::Array(nested) => {
4777                                                    result.extend(nested.iter().cloned())
4778                                                }
4779                                                JValue::Null => {}
4780                                                other => result.push(other),
4781                                            }
4782                                        }
4783                                        _ => {}
4784                                    }
4785                                }
4786
4787                                JValue::array(result)
4788                            }
4789                        }
4790                        JValue::Null => JValue::Null,
4791                        // Accessing field on non-object returns undefined (not an error)
4792                        _ => JValue::Undefined,
4793                    }
4794                }
4795                AstNode::String(string_literal) => {
4796                    // String literal as a path step - evaluate as literal and apply stages
4797                    let stages = &step.stages;
4798                    let val = JValue::string(string_literal.clone());
4799
4800                    if !stages.is_empty() {
4801                        // Apply stages (predicates) to the string literal
4802                        let result = self.apply_stages(val, stages)?;
4803                        // Unwrap single-element arrays back to scalar
4804                        match result {
4805                            JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4806                            JValue::Array(arr) if arr.is_empty() => JValue::Null,
4807                            other => other,
4808                        }
4809                    } else {
4810                        val
4811                    }
4812                }
4813                AstNode::Predicate(pred_expr) => {
4814                    // Predicate in path - filter or index into current value
4815                    self.evaluate_predicate(&current, pred_expr)?
4816                }
4817                AstNode::ArrayGroup(elements) => {
4818                    // Array grouping: map expression over array but keep results grouped
4819                    // .[expr] means evaluate expr for each array element
4820                    match &current {
4821                        JValue::Array(arr) => {
4822                            let mut result = Vec::new();
4823                            for item in arr.iter() {
4824                                // For each array item, evaluate all elements and collect results
4825                                let mut group_values = Vec::new();
4826                                for element in elements {
4827                                    let value = self.evaluate_internal(element, item)?;
4828                                    // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
4829                                    // This ensures [[expr]] produces properly nested arrays
4830                                    let should_preserve_array = matches!(
4831                                        element,
4832                                        AstNode::Array(_) | AstNode::ArrayGroup(_)
4833                                    );
4834
4835                                    if should_preserve_array {
4836                                        // Keep the array as a single element to preserve nesting
4837                                        group_values.push(value);
4838                                    } else {
4839                                        // Flatten the value into group_values
4840                                        match value {
4841                                            JValue::Array(arr) => {
4842                                                group_values.extend(arr.iter().cloned())
4843                                            }
4844                                            other => group_values.push(other),
4845                                        }
4846                                    }
4847                                }
4848                                // Each array element gets its own sub-array with all values
4849                                result.push(JValue::array(group_values));
4850                            }
4851                            // jsonata-js's evaluateStep: when this is the path's last
4852                            // step and mapping produced exactly one constructed
4853                            // sub-array, that sub-array IS the path result directly
4854                            // (not wrapped in an outer singleton array) — e.g.
4855                            // `$.[value,epochSeconds]` over a 1-element array yields
4856                            // `[3, 1578381600]`, not `[[3, 1578381600]]`.
4857                            if is_last_step && result.len() == 1 {
4858                                result.into_iter().next().unwrap()
4859                            } else {
4860                                JValue::array(result)
4861                            }
4862                        }
4863                        _ => {
4864                            // For non-arrays, just evaluate the array constructor normally
4865                            let mut result = Vec::new();
4866                            for element in elements {
4867                                let value = self.evaluate_internal(element, &current)?;
4868                                result.push(value);
4869                            }
4870                            JValue::array(result)
4871                        }
4872                    }
4873                }
4874                AstNode::FunctionApplication(expr) => {
4875                    // Function application: map expr over the current value
4876                    // .(expr) means evaluate expr for each element, with $ bound to that element
4877                    // Null/undefined results are filtered out
4878                    //
4879                    // When this parenthesized step is itself tuple-carrying (its
4880                    // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
4881                    // keep the inner path's tuple wrappers so their `!label`
4882                    // bindings survive to the following object/`%` step; the
4883                    // end-of-path projection (or a later consumer) unwraps them.
4884                    let saved_keep = self.keep_tuple_stream;
4885                    if step.is_tuple {
4886                        self.keep_tuple_stream = true;
4887                    }
4888                    let fa_result = match &current {
4889                        JValue::Array(arr) => {
4890                            // Produce the mapped result (compiled fast path or tree-walker fallback).
4891                            // Do NOT return early — singleton unwrapping is applied by the outer
4892                            // path evaluation code after all steps are processed.
4893                            let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
4894                            {
4895                                let shape = arr.first().and_then(build_shape_cache);
4896                                let mut result = Vec::with_capacity(arr.len());
4897                                for item in arr.iter() {
4898                                    let value = if let Some(ref s) = shape {
4899                                        eval_compiled_shaped(&compiled, item, None, s)?
4900                                    } else {
4901                                        eval_compiled(&compiled, item, None)?
4902                                    };
4903                                    if !value.is_null() && !value.is_undefined() {
4904                                        result.push(value);
4905                                    }
4906                                }
4907                                result
4908                            } else {
4909                                let mut result = Vec::new();
4910                                for item in arr.iter() {
4911                                    // Save the current $ binding
4912                                    let saved_dollar = self.context.lookup("$").cloned();
4913
4914                                    // Bind $ to the current item
4915                                    self.context.bind("$".to_string(), item.clone());
4916
4917                                    // Evaluate the expression in the context of this item
4918                                    let value = self.evaluate_internal(expr, item)?;
4919
4920                                    // Restore the previous $ binding
4921                                    if let Some(saved) = saved_dollar {
4922                                        self.context.bind("$".to_string(), saved);
4923                                    } else {
4924                                        self.context.unbind("$");
4925                                    }
4926
4927                                    // Only include non-null/undefined values
4928                                    if !value.is_null() && !value.is_undefined() {
4929                                        result.push(value);
4930                                    }
4931                                }
4932                                result
4933                            };
4934                            // Don't do singleton unwrapping here - let the path result
4935                            // handling deal with it, which respects has_explicit_array_keep
4936                            JValue::array(mapped)
4937                        }
4938                        _ => {
4939                            // For non-arrays, bind $ and evaluate
4940                            let saved_dollar = self.context.lookup("$").cloned();
4941                            self.context.bind("$".to_string(), current.clone());
4942
4943                            let value = self.evaluate_internal(expr, &current)?;
4944
4945                            if let Some(saved) = saved_dollar {
4946                                self.context.bind("$".to_string(), saved);
4947                            } else {
4948                                self.context.unbind("$");
4949                            }
4950
4951                            value
4952                        }
4953                    };
4954                    self.keep_tuple_stream = saved_keep;
4955                    fa_result
4956                }
4957                AstNode::Sort { terms, .. } => {
4958                    // Sort as a path step - sort 'current' by the terms
4959                    self.evaluate_sort(&current, terms)?
4960                }
4961                // Handle complex path steps (e.g., computed properties, object construction)
4962                _ => {
4963                    let saved_keep = self.keep_tuple_stream;
4964                    if step.is_tuple {
4965                        self.keep_tuple_stream = true;
4966                    }
4967                    let v = self.evaluate_path_step(&step.node, &current, data);
4968                    self.keep_tuple_stream = saved_keep;
4969                    v?
4970                }
4971            };
4972        }
4973
4974        // End-of-path tuple projection, mirroring jsonata-js evaluatePath
4975        // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
4976        // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
4977        // wrappers are internal bookkeeping and must not escape into an enclosing
4978        // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
4979        // `=` compare wrapper objects and always yield false). Suppressed only for
4980        // the two consumers that read the carried bindings directly off the
4981        // wrappers (Sort input, ObjectTransform/group-by input), which set
4982        // `keep_tuple_stream`. The top-level `evaluate()` still runs
4983        // `unwrap_tuple_output` as a backstop for wrappers nested inside
4984        // constructed output.
4985        if !self.keep_tuple_stream {
4986            if let JValue::Array(arr) = &current {
4987                let is_tuple_stream = arr.first().is_some_and(|f| {
4988                    matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
4989                });
4990                if is_tuple_stream {
4991                    let projected: Vec<JValue> = arr
4992                        .iter()
4993                        .map(|t| match t {
4994                            JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
4995                            other => other.clone(),
4996                        })
4997                        .collect();
4998                    current = JValue::array(projected);
4999                }
5000            }
5001        }
5002
5003        // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5004        // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5005
5006        // Check for explicit array-keeping operations. Empty predicate `[]` can
5007        // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5008        // stage; it also counts when it sits inside a `Sort` step's input path
5009        // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5010        // the sort and the trailing index so the singleton stays `[4]`.
5011        let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5012
5013        // Unwrap when:
5014        // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5015        // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5016        //    Note: did_array_mapping is reset to false when extracting from a single object,
5017        //    so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5018        // BUT NOT when there's an explicit array-keeping operation
5019        //
5020        // Important: We DON'T unwrap just because original data was an array - what matters is
5021        // whether the final extraction was from an array mapping context or a single object.
5022        let should_unwrap = !has_explicit_array_keep
5023            && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5024
5025        let result = match &current {
5026            // An empty result sequence is "no value" -> undefined (jsonata-js
5027            // treats an empty sequence, e.g. from a filter that matched nothing,
5028            // as undefined so a following `.field` and object/array construction
5029            // drop it rather than keeping an explicit null). `[]` array-keep is
5030            // handled separately above via has_explicit_array_keep.
5031            JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5032            // Unwrap singleton arrays when appropriate
5033            JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5034            // Keep arrays otherwise
5035            _ => current,
5036        };
5037
5038        // An explicit `[]` keep-array forces the result to remain an array even
5039        // after a later singleton index collapses it to a scalar (jsonata's
5040        // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5041        let result = if has_explicit_array_keep
5042            && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5043        {
5044            JValue::array(vec![result])
5045        } else {
5046            result
5047        };
5048
5049        Ok(result)
5050    }
5051
5052    /// True when a path step carries a tuple-binding flag (`@$var` focus,
5053    /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5054    /// produce/extend a tuple stream rather than be evaluated as a plain step.
5055    ///
5056    fn step_creates_tuple(step: &PathStep) -> bool {
5057        step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5058    }
5059
5060    /// True when a path contains an explicit empty predicate `[]` (keep-array),
5061    /// either directly as a step/stage or nested inside a `Sort` step's input
5062    /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5063    /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5064    /// -> `[4]`).
5065    fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5066        steps.iter().any(|step| {
5067            if let AstNode::Predicate(pred) = &step.node {
5068                if matches!(**pred, AstNode::Boolean(true)) {
5069                    return true;
5070                }
5071            }
5072            if step.stages.iter().any(
5073                |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5074            ) {
5075                return true;
5076            }
5077            if let AstNode::Sort { input, .. } = &step.node {
5078                if let AstNode::Path { steps: inner } = input.as_ref() {
5079                    return Self::path_keeps_singleton_array(inner);
5080                }
5081            }
5082            false
5083        })
5084    }
5085
5086    /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5087    /// scope, saving whatever was previously bound under each of those names
5088    /// so [`TupleKeyBindings::restore`] can put it back afterward.
5089    ///
5090    /// This is the single shared implementation of the
5091    /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5092    /// pattern that recurs across `create_tuple_stream`,
5093    /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5094    /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5095    /// none of those call sites can regress to a blind `unbind` (which
5096    /// deletes rather than restores a same-named outer `:=` binding that was
5097    /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5098    /// binding silently clobbering an outer variable of the same name).
5099    fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5100        let mut saved = Vec::new();
5101        for (key, value) in tuple_obj.iter() {
5102            let name = if let Some(n) = key.strip_prefix('$') {
5103                if n.is_empty() {
5104                    continue;
5105                }
5106                n.to_string()
5107            } else if key.starts_with('!') {
5108                key.clone()
5109            } else {
5110                continue;
5111            };
5112            saved.push((name.clone(), self.context.lookup(&name).cloned()));
5113            self.context.bind(name, value.clone());
5114        }
5115        TupleKeyBindings { saved }
5116    }
5117
5118    /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5119    /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5120    /// vector holds `JValue::Object` tuple wrappers of the shape
5121    /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5122    /// which downstream steps consume via the existing tuple-aware handling in
5123    /// `evaluate_path`.
5124    ///
5125    /// `input` is the previous step's result: either an already-built tuple
5126    /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5127    /// plain value/array entering tuple mode for the first time (each item
5128    /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5129    ///
5130    /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5131    /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5132    /// fast paths only *rebuild* a wrapper around a value pulled from an input
5133    /// element that is already `__tuple__`-tagged, which can only be true if a
5134    /// `create_tuple_stream` call already ran earlier in this evaluation and set
5135    /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5136    /// fire on a value that did NOT come from an existing tuple stream, it must
5137    /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5138    /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5139    fn create_tuple_stream(
5140        &mut self,
5141        step: &PathStep,
5142        input: &JValue,
5143        is_first_path_step: bool,
5144    ) -> Result<Vec<JValue>, EvaluatorError> {
5145        use std::rc::Rc;
5146
5147        // Mark that this evaluate() call produced tuple wrappers, so the
5148        // top-level `evaluate()` knows to run the output-unwrap pass.
5149        self.tuple_stream_created = true;
5150
5151        // Gather the incoming tuple bindings.
5152        let is_tuple_input = matches!(
5153            input,
5154            JValue::Array(arr) if arr.first().is_some_and(|f| {
5155                matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5156            })
5157        );
5158        let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5159            match input {
5160                JValue::Array(arr) => arr
5161                    .iter()
5162                    .filter_map(|t| match t {
5163                        JValue::Object(o) => Some(o.clone()),
5164                        _ => None,
5165                    })
5166                    .collect(),
5167                _ => unreachable!(),
5168            }
5169        } else {
5170            let items: Vec<JValue> = match input {
5171                // Mirrors jsonata-js evaluatePath's inputSequence rule
5172                // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5173                // when the path's FIRST step is a variable reference (`$`/`$$`) the
5174                // input array is taken as a SINGLE sequence value
5175                // (`createSequence(input)`) rather than iterated per-element. We
5176                // only need this for a leading INDEX bind (`$#$pos`): the whole
5177                // array becomes one incoming tuple whose `@` is the array, then
5178                // the inner position counter walks its elements so `$pos` runs
5179                // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5180                // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5181                // the step input, so a single binding would yield one copy of the
5182                // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5183                // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5184                // `$.$#$pos` (a later step) still iterates per-element.
5185                JValue::Array(arr)
5186                    if !(is_first_path_step
5187                        && matches!(&step.node, AstNode::Variable(_))
5188                        && step.index_var.is_some()) =>
5189                {
5190                    arr.iter().cloned().collect()
5191                }
5192                single => vec![single.clone()],
5193            };
5194            items
5195                .into_iter()
5196                .map(|item| {
5197                    let mut wrapper = IndexMap::new();
5198                    wrapper.insert("@".to_string(), item);
5199                    wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5200                    Rc::new(wrapper)
5201                })
5202                .collect()
5203        };
5204
5205        // A sort step in a tuple stream orders the WHOLE stream (not per element)
5206        // and re-tuples with the index = sorted position, mirroring jsonata-js
5207        // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5208        // array, then number the sorted values, then filter by `$pos`.
5209        if let AstNode::Sort { terms, .. } = &step.node {
5210            let stream = JValue::array(
5211                incoming
5212                    .iter()
5213                    .map(|t| JValue::object((**t).clone()))
5214                    .collect(),
5215            );
5216            // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5217            // carried keys bound), returning the wrappers in sorted order.
5218            let sorted = self.evaluate_sort(&stream, terms)?;
5219            let sorted_arr: Vec<JValue> = match sorted {
5220                JValue::Array(a) => a.iter().cloned().collect(),
5221                JValue::Null | JValue::Undefined => Vec::new(),
5222                other => vec![other],
5223            };
5224            let mut result = Vec::new();
5225            for (ss, elem) in sorted_arr.into_iter().enumerate() {
5226                let mut new_tuple = match elem {
5227                    JValue::Object(o) => (*o).clone(),
5228                    other => {
5229                        let mut m = IndexMap::new();
5230                        m.insert("@".to_string(), other);
5231                        m
5232                    }
5233                };
5234                if let Some(index_var) = &step.index_var {
5235                    new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
5236                }
5237                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5238                result.push(JValue::object(new_tuple));
5239            }
5240            return Ok(result);
5241        }
5242
5243        let mut result = Vec::new();
5244        for tuple_obj in incoming {
5245            // Bind every carried tuple key into a real scope frame so the step
5246            // expression can see prior focus/index/ancestor bindings, mirroring
5247            // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
5248            // Saves/restores rather than blindly unbinding, so a tuple key
5249            // whose name collides with a live outer `:=` binding doesn't get
5250            // deleted once this tuple row's evaluation is done.
5251            let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
5252
5253            let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
5254            let step_value = self.evaluate_internal(&step.node, &actual_data);
5255
5256            let mut step_value = step_value?;
5257            // When the step carries an ORDERED index stage (a second `#$var`,
5258            // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
5259            // BUILT tuple stream in order (filter then re-number) so the filter
5260            // sees the per-tuple focus/index bindings and each index reflects the
5261            // position at its point in the sequence. Those steps defer all stage
5262            // application to `apply_tuple_stages` after the stream is built.
5263            let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
5264            if !step.stages.is_empty() && !has_index_stage {
5265                // A `%` inside a filter predicate refers to the ancestry of
5266                // THIS step (its own input for a level-1 `%`, or an earlier
5267                // step's input for a `%.%` chain). ast_transform tags this step
5268                // with `ancestor_label`; bind it to the step's input so the
5269                // level-1 `%` resolves. The `%.%` chain's deeper references use
5270                // labels carried in the INCOMING tuple, so those bindings
5271                // (`tuple_bindings`) must stay live through `apply_stages` --
5272                // their restore is deferred until after it (previously they
5273                // were unbound first, which silently broke `%.%` inside
5274                // predicates).
5275                let own_label = match &step.ancestor_label {
5276                    Some(label) if !tuple_bindings.contains(label) => {
5277                        self.context.bind(label.clone(), actual_data.clone());
5278                        Some(label.clone())
5279                    }
5280                    _ => None,
5281                };
5282                step_value = self.apply_stages(step_value, &step.stages)?;
5283                if let Some(label) = own_label {
5284                    self.context.unbind(&label);
5285                }
5286            }
5287
5288            tuple_bindings.restore(self);
5289
5290            let row: Vec<JValue> = match step_value {
5291                JValue::Undefined => continue,
5292                JValue::Array(arr) => arr.iter().cloned().collect(),
5293                other => vec![other],
5294            };
5295
5296            for (position, value) in row.into_iter().enumerate() {
5297                if value.is_undefined() {
5298                    continue;
5299                }
5300                let mut new_tuple = (*tuple_obj).clone();
5301                if let Some(focus_var) = &step.focus {
5302                    // Focus binding keeps `@` as this step's INPUT (already carried
5303                    // in the cloned tuple) and binds the result to `$focus`,
5304                    // matching jsonata-js: `tuple[expr.focus] = res[bb];
5305                    // tuple['@'] = tupleBindings[ee]['@'];`.
5306                    new_tuple.insert(format!("${}", focus_var), value);
5307                } else {
5308                    new_tuple.insert("@".to_string(), value);
5309                }
5310                if let Some(index_var) = &step.index_var {
5311                    // Index binding records the position of this value WITHIN the
5312                    // per-binding result row (jsonata-js evaluateTupleStep: the
5313                    // inner `bb` counter, `tuple[expr.index] = bb`), which resets
5314                    // for each incoming tuple.
5315                    new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
5316                }
5317                if let Some(ancestor_label) = &step.ancestor_label {
5318                    // `%` ancestor: preserve this step's INPUT under the label.
5319                    new_tuple.insert(ancestor_label.clone(), actual_data.clone());
5320                }
5321                new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5322                result.push(JValue::object(new_tuple));
5323            }
5324        }
5325
5326        // Apply ordered filter/index stages to the built tuple stream when a
5327        // second index binding deferred them (see the has_index_stage comment
5328        // in the build loop above).
5329        if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
5330            result = self.apply_tuple_stages(result, &step.stages)?;
5331        }
5332
5333        Ok(result)
5334    }
5335
5336    /// Apply a step's stages, in order, to an already-built tuple stream --
5337    /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
5338    /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
5339    /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
5340    /// stage sets its variable on every surviving tuple to that tuple's position
5341    /// in the CURRENT stream. Used for steps carrying a second `#$var` index
5342    /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
5343    /// pre-filter position and `$ib2` the post-filter position.
5344    fn apply_tuple_stages(
5345        &mut self,
5346        mut tuples: Vec<JValue>,
5347        stages: &[Stage],
5348    ) -> Result<Vec<JValue>, EvaluatorError> {
5349        for stage in stages {
5350            match stage {
5351                Stage::Filter(pred) => {
5352                    let mut kept = Vec::with_capacity(tuples.len());
5353                    for tup in tuples.into_iter() {
5354                        let JValue::Object(obj) = &tup else {
5355                            continue;
5356                        };
5357                        // Bind this tuple's carried focus/index/ancestor keys so
5358                        // the predicate can reference them (save/restore rather
5359                        // than blind unbind -- see bind_tuple_keys).
5360                        let tuple_bindings = self.bind_tuple_keys(obj);
5361                        let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
5362                        let pred_res = self.evaluate_internal(pred, &at);
5363                        tuple_bindings.restore(self);
5364                        if self.is_truthy(&pred_res?) {
5365                            kept.push(tup);
5366                        }
5367                    }
5368                    tuples = kept;
5369                }
5370                Stage::Index(var) => {
5371                    for (pos, tup) in tuples.iter_mut().enumerate() {
5372                        if let JValue::Object(obj) = tup {
5373                            let mut m = (**obj).clone();
5374                            m.insert(format!("${}", var), JValue::from(pos as i64));
5375                            *tup = JValue::object(m);
5376                        }
5377                    }
5378                }
5379            }
5380        }
5381        Ok(tuples)
5382    }
5383
5384    /// Helper to evaluate a complex path step
5385    fn evaluate_path_step(
5386        &mut self,
5387        step: &AstNode,
5388        current: &JValue,
5389        original_data: &JValue,
5390    ) -> Result<JValue, EvaluatorError> {
5391        // Special case: array mapping with object construction
5392        // e.g., items.{"name": name, "price": price}
5393        if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
5394            match (current, step) {
5395                (JValue::Array(arr), AstNode::Object(pairs)) => {
5396                    // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
5397                    if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
5398                        let shape = arr.first().and_then(build_shape_cache);
5399                        let mut mapped = Vec::with_capacity(arr.len());
5400                        for item in arr.iter() {
5401                            let result = if let Some(ref s) = shape {
5402                                eval_compiled_shaped(&compiled, item, None, s)?
5403                            } else {
5404                                eval_compiled(&compiled, item, None)?
5405                            };
5406                            if !result.is_undefined() {
5407                                mapped.push(result);
5408                            }
5409                        }
5410                        return Ok(JValue::array(mapped));
5411                    }
5412                    // Fallback: full AST evaluation per element
5413                    let mapped: Result<Vec<JValue>, EvaluatorError> = arr
5414                        .iter()
5415                        .map(|item| self.evaluate_internal(step, item))
5416                        .collect();
5417                    Ok(JValue::array(mapped?))
5418                }
5419                _ => unreachable!(),
5420            }
5421        } else {
5422            // Special case: array.$ should map $ over the array, returning each element
5423            // e.g., [1, 2, 3].$ returns [1, 2, 3]
5424            if let AstNode::Variable(name) = step {
5425                if name.is_empty() {
5426                    // Bare $ - map over array if current is an array
5427                    if let JValue::Array(arr) = current {
5428                        // Map $ over each element - $ refers to each element in turn
5429                        return Ok(JValue::Array(arr.clone()));
5430                    } else {
5431                        // For non-arrays, $ refers to the current value
5432                        return Ok(current.clone());
5433                    }
5434                }
5435            }
5436
5437            // Special case: Variable access on tuple arrays (from index binding #$var)
5438            // When current is a tuple array, we need to evaluate the variable against each tuple
5439            // so that tuple bindings ($i, etc.) can be found
5440            if matches!(step, AstNode::Variable(_)) {
5441                if let JValue::Array(arr) = current {
5442                    // Check if this is a tuple array
5443                    let is_tuple_array = arr.first().is_some_and(|first| {
5444                        if let JValue::Object(obj) = first {
5445                            obj.get("__tuple__") == Some(&JValue::Bool(true))
5446                        } else {
5447                            false
5448                        }
5449                    });
5450
5451                    if is_tuple_array {
5452                        // Map the variable lookup over each tuple
5453                        let mut results = Vec::new();
5454                        for tuple in arr.iter() {
5455                            // Evaluate the variable in the context of this tuple
5456                            // This allows tuple bindings ($i, etc.) to be found
5457                            let val = self.evaluate_internal(step, tuple)?;
5458                            if !val.is_null() && !val.is_undefined() {
5459                                results.push(val);
5460                            }
5461                        }
5462                        return Ok(JValue::array(results));
5463                    }
5464                }
5465            }
5466
5467            // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
5468            // rather than being used to index/access the current value
5469            // e.g., items[price > 50] where [price > 50] is a filter operation
5470            // or $x.price where $x is a variable binding
5471            // or $$.field where $$ is the parent context
5472            // or [0..9] where it's an array constructor
5473            // or $^(field) where it's a sort operator
5474            // or (expr).field where (expr) is a block that evaluates to a value
5475            if matches!(
5476                step,
5477                AstNode::Binary { .. }
5478                    | AstNode::Function { .. }
5479                    | AstNode::Variable(_)
5480                    | AstNode::ParentVariable(_)
5481                    | AstNode::Parent(_)
5482                    | AstNode::Array(_)
5483                    | AstNode::Object(_)
5484                    | AstNode::Sort { .. }
5485                    | AstNode::Block(_)
5486            ) {
5487                // Evaluate the step in the context of original_data and return the result directly
5488                return self.evaluate_internal(step, original_data);
5489            }
5490
5491            // Standard path step evaluation for indexing/accessing current value
5492            let step_value = self.evaluate_internal(step, original_data)?;
5493            Ok(match (current, &step_value) {
5494                (JValue::Object(obj), JValue::String(key)) => {
5495                    obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
5496                }
5497                (JValue::Array(arr), JValue::Number(n)) => {
5498                    let index = *n as i64;
5499                    let len = arr.len() as i64;
5500
5501                    // Handle negative indexing (offset from end)
5502                    let actual_idx = if index < 0 { len + index } else { index };
5503
5504                    if actual_idx < 0 || actual_idx >= len {
5505                        JValue::Undefined
5506                    } else {
5507                        arr[actual_idx as usize].clone()
5508                    }
5509                }
5510                _ => JValue::Undefined,
5511            })
5512        }
5513    }
5514
5515    /// Evaluate a binary operation
5516    fn evaluate_binary_op(
5517        &mut self,
5518        op: crate::ast::BinaryOp,
5519        lhs: &AstNode,
5520        rhs: &AstNode,
5521        data: &JValue,
5522    ) -> Result<JValue, EvaluatorError> {
5523        use crate::ast::BinaryOp;
5524
5525        // Special handling for coalescing operator (??)
5526        // Returns right side if left is undefined (produces no value)
5527        // Note: literal null is a value, so it's NOT replaced
5528        if op == BinaryOp::Coalesce {
5529            // Try to evaluate the left side
5530            return match self.evaluate_internal(lhs, data) {
5531                Ok(value) => {
5532                    // Successfully evaluated to a value (even if it's null)
5533                    // Check if LHS is a literal null - keep it (null is a value, not undefined)
5534                    if matches!(lhs, AstNode::Null) {
5535                        Ok(value)
5536                    }
5537                    // For paths and variables, undefined (no match/unbound) - use RHS
5538                    else if value.is_undefined()
5539                        && (matches!(lhs, AstNode::Path { .. })
5540                            || matches!(lhs, AstNode::String(_))
5541                            || matches!(lhs, AstNode::Variable(_)))
5542                    {
5543                        self.evaluate_internal(rhs, data)
5544                    } else {
5545                        Ok(value)
5546                    }
5547                }
5548                Err(_) => {
5549                    // Evaluation failed (e.g., undefined variable) - use RHS
5550                    self.evaluate_internal(rhs, data)
5551                }
5552            };
5553        }
5554
5555        // Special handling for default operator (?:)
5556        // Returns right side if left is falsy or a non-value (like a function)
5557        if op == BinaryOp::Default {
5558            let left = self.evaluate_internal(lhs, data)?;
5559            if self.is_truthy_for_default(&left) {
5560                return Ok(left);
5561            }
5562            return self.evaluate_internal(rhs, data);
5563        }
5564
5565        // Special handling for chain/pipe operator (~>)
5566        // Pipes the LHS result to the RHS function as the first argument
5567        // e.g., expr ~> func(arg2) becomes func(expr, arg2)
5568        if op == BinaryOp::ChainPipe {
5569            // Handle regex on RHS - treat as $match(lhs, regex)
5570            if let AstNode::Regex { pattern, flags } = rhs {
5571                // Evaluate LHS
5572                let lhs_value = self.evaluate_internal(lhs, data)?;
5573                // Do regex match inline
5574                return match lhs_value {
5575                    JValue::String(s) => {
5576                        // Build the regex
5577                        let case_insensitive = flags.contains('i');
5578                        let regex_pattern = if case_insensitive {
5579                            format!("(?i){}", pattern)
5580                        } else {
5581                            pattern.clone()
5582                        };
5583                        match regex::Regex::new(&regex_pattern) {
5584                            Ok(re) => {
5585                                if let Some(m) = re.find(&s) {
5586                                    // Return match object
5587                                    let mut result = IndexMap::new();
5588                                    result.insert(
5589                                        "match".to_string(),
5590                                        JValue::string(m.as_str().to_string()),
5591                                    );
5592                                    result.insert(
5593                                        "start".to_string(),
5594                                        JValue::Number(m.start() as f64),
5595                                    );
5596                                    result
5597                                        .insert("end".to_string(), JValue::Number(m.end() as f64));
5598
5599                                    // Capture groups
5600                                    let mut groups = Vec::new();
5601                                    for cap in re.captures_iter(&s).take(1) {
5602                                        for i in 1..cap.len() {
5603                                            if let Some(c) = cap.get(i) {
5604                                                groups.push(JValue::string(c.as_str().to_string()));
5605                                            }
5606                                        }
5607                                    }
5608                                    if !groups.is_empty() {
5609                                        result.insert("groups".to_string(), JValue::array(groups));
5610                                    }
5611
5612                                    Ok(JValue::object(result))
5613                                } else {
5614                                    Ok(JValue::Null)
5615                                }
5616                            }
5617                            Err(e) => Err(EvaluatorError::EvaluationError(format!(
5618                                "Invalid regex: {}",
5619                                e
5620                            ))),
5621                        }
5622                    }
5623                    JValue::Null => Ok(JValue::Null),
5624                    _ => Err(EvaluatorError::TypeError(
5625                        "Left side of ~> /regex/ must be a string".to_string(),
5626                    )),
5627                };
5628            }
5629
5630            // Early check: if LHS evaluates to undefined, return undefined
5631            // This matches JSONata behavior where undefined ~> anyFunc returns undefined
5632            let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
5633            if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
5634                return Ok(JValue::Undefined);
5635            }
5636
5637            // Handle different RHS types
5638            match rhs {
5639                AstNode::Function {
5640                    name,
5641                    args,
5642                    is_builtin,
5643                } => {
5644                    // RHS is a function call
5645                    // Check if the function call has placeholder arguments (partial application)
5646                    let has_placeholder =
5647                        args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
5648
5649                    if has_placeholder {
5650                        // Partial application: replace the first placeholder with LHS value
5651                        let lhs_value = self.evaluate_internal(lhs, data)?;
5652                        let mut filled_args = Vec::new();
5653                        let mut lhs_used = false;
5654
5655                        for arg in args.iter() {
5656                            if matches!(arg, AstNode::Placeholder) && !lhs_used {
5657                                // Replace first placeholder with evaluated LHS
5658                                // We need to create a temporary binding to pass the value
5659                                let temp_name = format!("__pipe_arg_{}", filled_args.len());
5660                                self.context.bind(temp_name.clone(), lhs_value.clone());
5661                                filled_args.push(AstNode::Variable(temp_name));
5662                                lhs_used = true;
5663                            } else {
5664                                filled_args.push(arg.clone());
5665                            }
5666                        }
5667
5668                        // Evaluate the function with filled args
5669                        let result =
5670                            self.evaluate_function_call(name, &filled_args, *is_builtin, data);
5671
5672                        // Clean up temp bindings
5673                        for (i, arg) in args.iter().enumerate() {
5674                            if matches!(arg, AstNode::Placeholder) {
5675                                self.context.unbind(&format!("__pipe_arg_{}", i));
5676                            }
5677                        }
5678
5679                        // Unwrap singleton results from chain operator
5680                        return result.map(|v| self.unwrap_singleton(v));
5681                    } else {
5682                        // No placeholders: build args list with LHS as first argument
5683                        let mut all_args = vec![lhs.clone()];
5684                        all_args.extend_from_slice(args);
5685                        // Unwrap singleton results from chain operator
5686                        return self
5687                            .evaluate_function_call(name, &all_args, *is_builtin, data)
5688                            .map(|v| self.unwrap_singleton(v));
5689                    }
5690                }
5691                AstNode::Variable(var_name) => {
5692                    // RHS is a function reference (no parens)
5693                    // e.g., $average($tempReadings) ~> $round
5694                    let all_args = vec![lhs.clone()];
5695                    // Unwrap singleton results from chain operator
5696                    return self
5697                        .evaluate_function_call(var_name, &all_args, true, data)
5698                        .map(|v| self.unwrap_singleton(v));
5699                }
5700                AstNode::Binary {
5701                    op: BinaryOp::ChainPipe,
5702                    ..
5703                } => {
5704                    // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
5705                    // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
5706                    let lhs_value = self.evaluate_internal(lhs, data)?;
5707                    return self.evaluate_internal(rhs, &lhs_value);
5708                }
5709                AstNode::Transform { .. } => {
5710                    // RHS is a transform - invoke it with LHS as input
5711                    // Evaluate LHS first
5712                    let lhs_value = self.evaluate_internal(lhs, data)?;
5713
5714                    // Bind $ to the LHS value, then evaluate the transform
5715                    let saved_binding = self.context.lookup("$").cloned();
5716                    self.context.bind("$".to_string(), lhs_value.clone());
5717
5718                    let result = self.evaluate_internal(rhs, data);
5719
5720                    // Restore $ binding
5721                    if let Some(saved) = saved_binding {
5722                        self.context.bind("$".to_string(), saved);
5723                    } else {
5724                        self.context.unbind("$");
5725                    }
5726
5727                    // Unwrap singleton results from chain operator
5728                    return result.map(|v| self.unwrap_singleton(v));
5729                }
5730                AstNode::Lambda {
5731                    params,
5732                    body,
5733                    signature,
5734                    thunk,
5735                } => {
5736                    // RHS is a lambda - invoke it with LHS as argument
5737                    let lhs_value = self.evaluate_internal(lhs, data)?;
5738                    // Unwrap singleton results from chain operator
5739                    return self
5740                        .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
5741                        .map(|v| self.unwrap_singleton(v));
5742                }
5743                AstNode::Path { steps } => {
5744                    // RHS is a path expression (e.g., function call with predicate: $map($f)[])
5745                    // If the first step is a function call, we need to add LHS as first argument
5746                    if let Some(first_step) = steps.first() {
5747                        match &first_step.node {
5748                            AstNode::Function {
5749                                name,
5750                                args,
5751                                is_builtin,
5752                            } => {
5753                                // Prepend LHS to the function arguments
5754                                let mut all_args = vec![lhs.clone()];
5755                                all_args.extend_from_slice(args);
5756
5757                                // Call the function
5758                                let mut result = self.evaluate_function_call(
5759                                    name,
5760                                    &all_args,
5761                                    *is_builtin,
5762                                    data,
5763                                )?;
5764
5765                                // Apply stages from the first step (e.g., predicates)
5766                                for stage in &first_step.stages {
5767                                    match stage {
5768                                        Stage::Filter(filter_expr) => {
5769                                            result = self.evaluate_predicate_as_stage(
5770                                                &result,
5771                                                filter_expr,
5772                                            )?;
5773                                        }
5774                                        Stage::Index(_) => {}
5775                                    }
5776                                }
5777
5778                                // Apply remaining path steps if any
5779                                if steps.len() > 1 {
5780                                    let remaining_path = AstNode::Path {
5781                                        steps: steps[1..].to_vec(),
5782                                    };
5783                                    result = self.evaluate_internal(&remaining_path, &result)?;
5784                                }
5785
5786                                // Unwrap singleton results from chain operator, unless there are stages
5787                                // Stages (like predicates) indicate we want to preserve array structure
5788                                if !first_step.stages.is_empty() || steps.len() > 1 {
5789                                    return Ok(result);
5790                                } else {
5791                                    return Ok(self.unwrap_singleton(result));
5792                                }
5793                            }
5794                            AstNode::Variable(var_name) => {
5795                                // Variable that should resolve to a function
5796                                let all_args = vec![lhs.clone()];
5797                                let mut result =
5798                                    self.evaluate_function_call(var_name, &all_args, true, data)?;
5799
5800                                // Apply stages from the first step
5801                                for stage in &first_step.stages {
5802                                    match stage {
5803                                        Stage::Filter(filter_expr) => {
5804                                            result = self.evaluate_predicate_as_stage(
5805                                                &result,
5806                                                filter_expr,
5807                                            )?;
5808                                        }
5809                                        Stage::Index(_) => {}
5810                                    }
5811                                }
5812
5813                                // Apply remaining path steps if any
5814                                if steps.len() > 1 {
5815                                    let remaining_path = AstNode::Path {
5816                                        steps: steps[1..].to_vec(),
5817                                    };
5818                                    result = self.evaluate_internal(&remaining_path, &result)?;
5819                                }
5820
5821                                // Unwrap singleton results from chain operator, unless there are stages
5822                                // Stages (like predicates) indicate we want to preserve array structure
5823                                if !first_step.stages.is_empty() || steps.len() > 1 {
5824                                    return Ok(result);
5825                                } else {
5826                                    return Ok(self.unwrap_singleton(result));
5827                                }
5828                            }
5829                            _ => {
5830                                // Other path types - just evaluate normally with LHS as context
5831                                let lhs_value = self.evaluate_internal(lhs, data)?;
5832                                return self
5833                                    .evaluate_internal(rhs, &lhs_value)
5834                                    .map(|v| self.unwrap_singleton(v));
5835                            }
5836                        }
5837                    }
5838
5839                    // Empty path? Shouldn't happen, but handle it
5840                    let lhs_value = self.evaluate_internal(lhs, data)?;
5841                    return self
5842                        .evaluate_internal(rhs, &lhs_value)
5843                        .map(|v| self.unwrap_singleton(v));
5844                }
5845                _ => {
5846                    return Err(EvaluatorError::TypeError(
5847                        "Right side of ~> must be a function call or function reference"
5848                            .to_string(),
5849                    ));
5850                }
5851            }
5852        }
5853
5854        // Special handling for variable binding (:=)
5855        if op == BinaryOp::ColonEqual {
5856            // Extract variable name from LHS
5857            let var_name = match lhs {
5858                AstNode::Variable(name) => name.clone(),
5859                _ => {
5860                    return Err(EvaluatorError::TypeError(
5861                        "Left side of := must be a variable".to_string(),
5862                    ))
5863                }
5864            };
5865
5866            // Check if RHS is a lambda - store it specially
5867            if let AstNode::Lambda {
5868                params,
5869                body,
5870                signature,
5871                thunk,
5872            } = rhs
5873            {
5874                // Store the lambda AST for later invocation
5875                // Capture only the free variables referenced by the lambda body
5876                let captured_env = self.capture_environment_for(body, params);
5877                let compiled_body = if !thunk {
5878                    let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
5879                    try_compile_expr_with_allowed_vars(body, &var_refs)
5880                } else {
5881                    None
5882                };
5883                let stored_lambda = StoredLambda {
5884                    params: params.clone(),
5885                    body: (**body).clone(),
5886                    compiled_body,
5887                    signature: signature.clone(),
5888                    captured_env,
5889                    captured_data: Some(data.clone()),
5890                    thunk: *thunk,
5891                };
5892                let lambda_params = stored_lambda.params.clone();
5893                let lambda_sig = stored_lambda.signature.clone();
5894                self.context.bind_lambda(var_name.clone(), stored_lambda);
5895
5896                // Return a lambda marker value (include _lambda_id so it can be found later)
5897                let lambda_repr = JValue::lambda(
5898                    var_name.as_str(),
5899                    lambda_params,
5900                    Some(var_name.clone()),
5901                    lambda_sig,
5902                );
5903                return Ok(lambda_repr);
5904            }
5905
5906            // Check if RHS is a pure function composition (ChainPipe between function references)
5907            // e.g., $uppertrim := $trim ~> $uppercase
5908            // This creates a lambda that composes the functions.
5909            // But NOT for data ~> function, which should be evaluated immediately.
5910            // e.g., $result := data ~> $map($fn) should evaluate the pipe
5911            if let AstNode::Binary {
5912                op: BinaryOp::ChainPipe,
5913                lhs: chain_lhs,
5914                rhs: chain_rhs,
5915            } = rhs
5916            {
5917                // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
5918                // If LHS is data (array, object, function call result, etc.), evaluate the pipe
5919                let is_function_composition = match chain_lhs.as_ref() {
5920                    // LHS is a function reference like $trim or $sum
5921                    AstNode::Variable(name)
5922                        if self.is_builtin_function(name)
5923                            || self.context.lookup_lambda(name).is_some() =>
5924                    {
5925                        true
5926                    }
5927                    // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
5928                    AstNode::Binary {
5929                        op: BinaryOp::ChainPipe,
5930                        ..
5931                    } => true,
5932                    // A function call with placeholder creates a partial application
5933                    // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
5934                    AstNode::Function { args, .. }
5935                        if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
5936                    {
5937                        true
5938                    }
5939                    // Anything else (data, function calls, arrays, etc.) is not pure composition
5940                    _ => false,
5941                };
5942
5943                if is_function_composition {
5944                    // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
5945                    // The original chain is $trim ~> $uppercase (left-associative)
5946                    // We want to create: ($ ~> $trim) ~> $uppercase
5947                    let param_name = "$".to_string();
5948
5949                    // First create $ ~> $trim
5950                    let first_pipe = AstNode::Binary {
5951                        op: BinaryOp::ChainPipe,
5952                        lhs: Box::new(AstNode::Variable(param_name.clone())),
5953                        rhs: chain_lhs.clone(),
5954                    };
5955
5956                    // Then wrap with ~> $uppercase (or the rest of the chain)
5957                    let composed_body = AstNode::Binary {
5958                        op: BinaryOp::ChainPipe,
5959                        lhs: Box::new(first_pipe),
5960                        rhs: chain_rhs.clone(),
5961                    };
5962
5963                    let stored_lambda = StoredLambda {
5964                        params: vec![param_name],
5965                        body: composed_body,
5966                        compiled_body: None, // ChainPipe body is not compilable
5967                        signature: None,
5968                        captured_env: self.capture_current_environment(),
5969                        captured_data: Some(data.clone()),
5970                        thunk: false,
5971                    };
5972                    self.context.bind_lambda(var_name.clone(), stored_lambda);
5973
5974                    // Return a lambda marker value (include _lambda_id for later lookup)
5975                    let lambda_repr = JValue::lambda(
5976                        var_name.as_str(),
5977                        vec!["$".to_string()],
5978                        Some(var_name.clone()),
5979                        None::<String>,
5980                    );
5981                    return Ok(lambda_repr);
5982                }
5983                // If not function composition, fall through to normal evaluation below
5984            }
5985
5986            // Evaluate the RHS
5987            let value = self.evaluate_internal(rhs, data)?;
5988
5989            // If the value is a lambda, copy the stored lambda to the new variable name
5990            if let Some(stored) = self.lookup_lambda_from_value(&value) {
5991                self.context.bind_lambda(var_name.clone(), stored);
5992            }
5993
5994            // Bind even if undefined (null) so inner scopes can shadow outer variables
5995            self.context.bind(var_name, value.clone());
5996            return Ok(value);
5997        }
5998
5999        // Special handling for 'In' operator - check for array filtering
6000        // Must evaluate lhs first to determine if this is array filtering
6001        if op == BinaryOp::In {
6002            let left = self.evaluate_internal(lhs, data)?;
6003
6004            // Check if this is array filtering: array[predicate]
6005            if matches!(left, JValue::Array(_)) {
6006                // Try evaluating rhs in current context to see if it's a simple index
6007                let right_result = self.evaluate_internal(rhs, data);
6008
6009                if let Ok(JValue::Number(_)) = right_result {
6010                    // Simple numeric index: array[n]
6011                    return self.array_index(&left, &right_result.unwrap());
6012                } else {
6013                    // This is array filtering: array[predicate]
6014                    // Evaluate the predicate for each array item
6015                    return self.array_filter(lhs, rhs, &left, data);
6016                }
6017            }
6018        }
6019
6020        // Special handling for logical operators (short-circuit evaluation)
6021        if op == BinaryOp::And {
6022            let left = self.evaluate_internal(lhs, data)?;
6023            if !self.is_truthy(&left) {
6024                // Short-circuit: if left is falsy, return false without evaluating right
6025                return Ok(JValue::Bool(false));
6026            }
6027            let right = self.evaluate_internal(rhs, data)?;
6028            return Ok(JValue::Bool(self.is_truthy(&right)));
6029        }
6030
6031        if op == BinaryOp::Or {
6032            let left = self.evaluate_internal(lhs, data)?;
6033            if self.is_truthy(&left) {
6034                // Short-circuit: if left is truthy, return true without evaluating right
6035                return Ok(JValue::Bool(true));
6036            }
6037            let right = self.evaluate_internal(rhs, data)?;
6038            return Ok(JValue::Bool(self.is_truthy(&right)));
6039        }
6040
6041        // Check if operands are explicit null literals (vs undefined from variables)
6042        let left_is_explicit_null = matches!(lhs, AstNode::Null);
6043        let right_is_explicit_null = matches!(rhs, AstNode::Null);
6044
6045        // Standard evaluation: evaluate both operands
6046        let left = self.evaluate_internal(lhs, data)?;
6047        let right = self.evaluate_internal(rhs, data)?;
6048
6049        match op {
6050            BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6051            BinaryOp::Subtract => {
6052                self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6053            }
6054            BinaryOp::Multiply => {
6055                self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6056            }
6057            BinaryOp::Divide => {
6058                self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6059            }
6060            BinaryOp::Modulo => {
6061                self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6062            }
6063
6064            BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
6065            BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
6066            BinaryOp::LessThan => {
6067                self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6068            }
6069            BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6070                &left,
6071                &right,
6072                left_is_explicit_null,
6073                right_is_explicit_null,
6074            ),
6075            BinaryOp::GreaterThan => {
6076                self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6077            }
6078            BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6079                &left,
6080                &right,
6081                left_is_explicit_null,
6082                right_is_explicit_null,
6083            ),
6084
6085            // And/Or handled above with short-circuit evaluation
6086            BinaryOp::And | BinaryOp::Or => unreachable!(),
6087
6088            BinaryOp::Concatenate => self.concatenate(&left, &right),
6089            BinaryOp::Range => self.range(&left, &right),
6090            BinaryOp::In => self.in_operator(&left, &right),
6091
6092            // Focus binding: should be resolved by ast_transform pass (Task 2)
6093            BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6094                "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6095            )),
6096
6097            // Index binding: should be resolved by ast_transform pass (Task 4,
6098            // which retired the dedicated AstNode::IndexBind variant in favor
6099            // of this generic Binary marker, mirroring FocusBind above)
6100            BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6101                "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6102            )),
6103
6104            // These operators are all handled as special cases earlier in evaluate_binary_op
6105            BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6106                unreachable!()
6107            }
6108        }
6109    }
6110
6111    /// Evaluate a unary operation
6112    fn evaluate_unary_op(
6113        &mut self,
6114        op: crate::ast::UnaryOp,
6115        operand: &AstNode,
6116        data: &JValue,
6117    ) -> Result<JValue, EvaluatorError> {
6118        use crate::ast::UnaryOp;
6119
6120        let value = self.evaluate_internal(operand, data)?;
6121
6122        match op {
6123            UnaryOp::Negate => match value {
6124                // undefined returns undefined
6125                JValue::Null | JValue::Undefined => Ok(JValue::Null),
6126                JValue::Number(n) => Ok(JValue::Number(-n)),
6127                _ => Err(EvaluatorError::TypeError(
6128                    "D1002: Cannot negate non-number value".to_string(),
6129                )),
6130            },
6131            UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6132        }
6133    }
6134
6135    /// Try to fuse an aggregate function call with its Path argument.
6136    /// Handles patterns like:
6137    /// - $sum(arr.field) → iterate arr, extract field, accumulate
6138    /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6139    ///
6140    /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6141    fn try_fused_aggregate(
6142        &mut self,
6143        name: &str,
6144        arg: &AstNode,
6145        data: &JValue,
6146    ) -> Result<Option<JValue>, EvaluatorError> {
6147        // Only applies to numeric aggregates
6148        if !matches!(name, "sum" | "max" | "min" | "average") {
6149            return Ok(None);
6150        }
6151
6152        // Argument must be a Path
6153        let AstNode::Path { steps } = arg else {
6154            return Ok(None);
6155        };
6156
6157        // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6158        // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6159        if steps.len() != 2 {
6160            return Ok(None);
6161        }
6162
6163        // Last step must be a simple Name (the field to extract)
6164        let field_step = &steps[1];
6165        if !field_step.stages.is_empty() {
6166            return Ok(None);
6167        }
6168        let AstNode::Name(extract_field) = &field_step.node else {
6169            return Ok(None);
6170        };
6171
6172        // First step: Name with optional filter stage
6173        let arr_step = &steps[0];
6174        let AstNode::Name(arr_name) = &arr_step.node else {
6175            return Ok(None);
6176        };
6177
6178        // Get the source array from data
6179        let arr = match data {
6180            JValue::Object(obj) => match obj.get(arr_name) {
6181                Some(JValue::Array(arr)) => arr,
6182                _ => return Ok(None),
6183            },
6184            _ => return Ok(None),
6185        };
6186
6187        // Check for filter stage — try CompiledExpr for the predicate
6188        let filter_compiled = match arr_step.stages.as_slice() {
6189            [] => None,
6190            [Stage::Filter(pred)] => try_compile_expr(pred),
6191            _ => return Ok(None),
6192        };
6193        // If filter stage exists but wasn't compilable, bail out
6194        if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6195            return Ok(None);
6196        }
6197
6198        // Build shape cache for the array
6199        let shape = arr.first().and_then(build_shape_cache);
6200
6201        // Fused iteration: filter (optional) + extract + aggregate
6202        let mut total = 0.0f64;
6203        let mut count = 0usize;
6204        let mut max_val = f64::NEG_INFINITY;
6205        let mut min_val = f64::INFINITY;
6206        let mut has_any = false;
6207
6208        for item in arr.iter() {
6209            // Apply compiled filter if present
6210            if let Some(ref compiled) = filter_compiled {
6211                let result = if let Some(ref s) = shape {
6212                    eval_compiled_shaped(compiled, item, None, s)?
6213                } else {
6214                    eval_compiled(compiled, item, None)?
6215                };
6216                if !compiled_is_truthy(&result) {
6217                    continue;
6218                }
6219            }
6220
6221            // Extract field value
6222            let val = match item {
6223                JValue::Object(obj) => match obj.get(extract_field) {
6224                    Some(JValue::Number(n)) => *n,
6225                    Some(_) | None => continue, // Skip non-numeric / missing
6226                },
6227                _ => continue,
6228            };
6229
6230            has_any = true;
6231            match name {
6232                "sum" => total += val,
6233                "max" => max_val = max_val.max(val),
6234                "min" => min_val = min_val.min(val),
6235                "average" => {
6236                    total += val;
6237                    count += 1;
6238                }
6239                _ => unreachable!(),
6240            }
6241        }
6242
6243        if !has_any {
6244            return Ok(Some(match name {
6245                "sum" => JValue::from(0i64),
6246                "average" | "max" | "min" => JValue::Null,
6247                _ => unreachable!(),
6248            }));
6249        }
6250
6251        Ok(Some(match name {
6252            "sum" => JValue::Number(total),
6253            "max" => JValue::Number(max_val),
6254            "min" => JValue::Number(min_val),
6255            "average" => JValue::Number(total / count as f64),
6256            _ => unreachable!(),
6257        }))
6258    }
6259
6260    /// Evaluate a function call
6261    fn evaluate_function_call(
6262        &mut self,
6263        name: &str,
6264        args: &[AstNode],
6265        is_builtin: bool,
6266        data: &JValue,
6267    ) -> Result<JValue, EvaluatorError> {
6268        use crate::functions;
6269
6270        // Check for partial application (any argument is a Placeholder)
6271        let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6272        if has_placeholder {
6273            return self.create_partial_application(name, args, is_builtin, data);
6274        }
6275
6276        // FIRST check if this variable holds a function value (lambda or builtin reference)
6277        // This is critical for:
6278        // 1. Allowing function parameters to shadow stored lambdas
6279        //    (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
6280        // 2. Calling built-in functions passed as parameters
6281        //    (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
6282        if let Some(value) = self.context.lookup(name).cloned() {
6283            if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
6284                let mut evaluated_args = Vec::with_capacity(args.len());
6285                for arg in args {
6286                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6287                }
6288                return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6289            }
6290            if let JValue::Builtin { name: builtin_name } = &value {
6291                // This is a built-in function reference (e.g., $f bound to $sum)
6292                let mut evaluated_args = Vec::with_capacity(args.len());
6293                for arg in args {
6294                    evaluated_args.push(self.evaluate_internal(arg, data)?);
6295                }
6296                return self.call_builtin_with_values(builtin_name, &evaluated_args);
6297            }
6298        }
6299
6300        // THEN check if this is a stored lambda (user-defined function by name)
6301        // This only applies if not shadowed by a binding above
6302        if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
6303            let mut evaluated_args = Vec::with_capacity(args.len());
6304            for arg in args {
6305                evaluated_args.push(self.evaluate_internal(arg, data)?);
6306            }
6307            return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6308        }
6309
6310        // If the function was called without $ prefix and it's not a stored lambda,
6311        // it's an error (unknown function without $ prefix)
6312        if !is_builtin && name != "__lambda__" {
6313            return Err(EvaluatorError::ReferenceError(format!(
6314                "Unknown function: {}",
6315                name
6316            )));
6317        }
6318
6319        // Special handling for $exists function
6320        // It needs to know if the argument is explicit null vs undefined
6321        if name == "exists" && args.len() == 1 {
6322            let arg = &args[0];
6323
6324            // Check if it's an explicit null literal
6325            if matches!(arg, AstNode::Null) {
6326                return Ok(JValue::Bool(true)); // Explicit null exists
6327            }
6328
6329            // Check if it's a function reference
6330            if let AstNode::Variable(var_name) = arg {
6331                if self.is_builtin_function(var_name) {
6332                    return Ok(JValue::Bool(true)); // Built-in function exists
6333                }
6334
6335                // Check if it's a stored lambda
6336                if self.context.lookup_lambda(var_name).is_some() {
6337                    return Ok(JValue::Bool(true)); // Lambda exists
6338                }
6339
6340                // Check if the variable is defined
6341                if let Some(val) = self.context.lookup(var_name) {
6342                    // A variable bound to the undefined marker doesn't "exist"
6343                    if val.is_undefined() {
6344                        return Ok(JValue::Bool(false));
6345                    }
6346                    return Ok(JValue::Bool(true)); // Variable is defined (even if null)
6347                } else {
6348                    return Ok(JValue::Bool(false)); // Variable is undefined
6349                }
6350            }
6351
6352            // For other expressions, evaluate and check if non-null/non-undefined
6353            let value = self.evaluate_internal(arg, data)?;
6354            return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
6355        }
6356
6357        // Check if any arguments are undefined variables or undefined paths
6358        // Functions like $not() should return undefined when given undefined values
6359        for arg in args {
6360            // Check for undefined variable (e.g., $undefined_var)
6361            if let AstNode::Variable(var_name) = arg {
6362                // Skip built-in function names - they're function references, not undefined variables
6363                if !var_name.is_empty()
6364                    && !self.is_builtin_function(var_name)
6365                    && self.context.lookup(var_name).is_none()
6366                {
6367                    // Undefined variable - for functions that should propagate undefined
6368                    if propagates_undefined(name) {
6369                        return Ok(JValue::Null); // Return undefined
6370                    }
6371                }
6372            }
6373            // Check for simple field name (e.g., blah) that evaluates to undefined
6374            if let AstNode::Name(field_name) = arg {
6375                let field_exists =
6376                    matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
6377                if !field_exists && propagates_undefined(name) {
6378                    return Ok(JValue::Null);
6379                }
6380            }
6381            // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
6382            // Field accesses are represented as AstNode::Path. String literals should never
6383            // be checked for undefined propagation.
6384            // Check for Path expressions that evaluate to undefined
6385            if let AstNode::Path { steps } = arg {
6386                // For paths that evaluate to null, we need to determine if it's because:
6387                // 1. A field doesn't exist (undefined) - should propagate as undefined
6388                // 2. A field exists with value null - should throw T0410
6389                //
6390                // We can distinguish these by checking if the path is accessing a field
6391                // that doesn't exist on an object vs one that has an explicit null value.
6392                if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
6393                    // Path evaluated to null - now check if it's truly undefined
6394                    // For single-step paths, check if the field exists
6395                    if steps.len() == 1 {
6396                        // Get field name - could be Name (identifier) or String (quoted)
6397                        let field_name = match &steps[0].node {
6398                            AstNode::Name(n) => Some(n.as_str()),
6399                            AstNode::String(s) => Some(s.as_str()),
6400                            _ => None,
6401                        };
6402                        if let Some(field) = field_name {
6403                            match data {
6404                                JValue::Object(obj) => {
6405                                    if !obj.contains_key(field) {
6406                                        // Field doesn't exist - return undefined
6407                                        if propagates_undefined(name) {
6408                                            return Ok(JValue::Null);
6409                                        }
6410                                    }
6411                                    // Field exists with value null - continue to throw T0410
6412                                }
6413                                // Trying to access field on null data - return undefined
6414                                JValue::Null if propagates_undefined(name) => {
6415                                    return Ok(JValue::Null);
6416                                }
6417                                _ => {}
6418                            }
6419                        }
6420                    }
6421                    // For multi-step paths, check if any intermediate step failed
6422                    else if steps.len() > 1 {
6423                        // Evaluate each step to find where it breaks
6424                        let mut current = data;
6425                        let mut failed_due_to_missing_field = false;
6426
6427                        for (i, step) in steps.iter().enumerate() {
6428                            if let AstNode::Name(field_name) = &step.node {
6429                                match current {
6430                                    JValue::Object(obj) => {
6431                                        if let Some(val) = obj.get(field_name) {
6432                                            current = val;
6433                                        } else {
6434                                            // Field doesn't exist
6435                                            failed_due_to_missing_field = true;
6436                                            break;
6437                                        }
6438                                    }
6439                                    JValue::Array(_) => {
6440                                        // Array access - evaluate normally
6441                                        break;
6442                                    }
6443                                    JValue::Null => {
6444                                        // Hit null in the middle of the path
6445                                        if i > 0 {
6446                                            // Previous field had null value - not undefined
6447                                            failed_due_to_missing_field = false;
6448                                        }
6449                                        break;
6450                                    }
6451                                    _ => break,
6452                                }
6453                            }
6454                        }
6455
6456                        if failed_due_to_missing_field && propagates_undefined(name) {
6457                            return Ok(JValue::Null);
6458                        }
6459                    }
6460                }
6461            }
6462        }
6463
6464        // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
6465        // try to fuse filter+extract+aggregate into a single pass.
6466        if args.len() == 1 {
6467            if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
6468                return Ok(result);
6469            }
6470        }
6471
6472        let mut evaluated_args = Vec::with_capacity(args.len());
6473        for arg in args {
6474            evaluated_args.push(self.evaluate_internal(arg, data)?);
6475        }
6476
6477        // JSONata feature: when a function is called with no arguments but expects
6478        // at least one, use the current context value (data) as the implicit first argument
6479        // This also applies when functions expecting N arguments receive N-1 arguments,
6480        // in which case the context value becomes the first argument
6481        let context_functions_zero_arg = [
6482            "string",
6483            "number",
6484            "boolean",
6485            "uppercase",
6486            "lowercase",
6487            "fromMillis",
6488        ];
6489        let context_functions_missing_first = [
6490            "substringBefore",
6491            "substringAfter",
6492            "contains",
6493            "split",
6494            "replace",
6495        ];
6496
6497        if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
6498            // Use the current context value as the implicit argument
6499            evaluated_args.push(data.clone());
6500        } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
6501            // These functions expect 2+ arguments, but received 1
6502            // Only insert context if it's a compatible type (string for string functions)
6503            // Otherwise, let the function throw T0411 for wrong argument count
6504            if matches!(data, JValue::String(_)) {
6505                evaluated_args.insert(0, data.clone());
6506            }
6507        }
6508
6509        // Special handling for $string() with no explicit arguments
6510        // After context insertion, check if the argument is null (undefined context)
6511        if name == "string"
6512            && args.is_empty()
6513            && !evaluated_args.is_empty()
6514            && evaluated_args[0].is_null()
6515        {
6516            // Context was null/undefined, so return undefined
6517            return Ok(JValue::Null);
6518        }
6519
6520        match name {
6521            "string" => {
6522                if evaluated_args.len() > 2 {
6523                    return Err(EvaluatorError::EvaluationError(
6524                        "string() takes at most 2 arguments".to_string(),
6525                    ));
6526                }
6527
6528                let prettify = if evaluated_args.len() == 2 {
6529                    match &evaluated_args[1] {
6530                        JValue::Bool(b) => Some(*b),
6531                        _ => {
6532                            return Err(EvaluatorError::TypeError(
6533                                "string() prettify parameter must be a boolean".to_string(),
6534                            ))
6535                        }
6536                    }
6537                } else {
6538                    None
6539                };
6540
6541                Ok(functions::string::string(&evaluated_args[0], prettify)?)
6542            }
6543            "length" => {
6544                if evaluated_args.len() != 1 {
6545                    return Err(EvaluatorError::EvaluationError(
6546                        "length() requires exactly 1 argument".to_string(),
6547                    ));
6548                }
6549                match &evaluated_args[0] {
6550                    JValue::String(s) => Ok(functions::string::length(s)?),
6551                    _ => Err(EvaluatorError::TypeError(
6552                        "T0410: Argument 1 of function length does not match function signature"
6553                            .to_string(),
6554                    )),
6555                }
6556            }
6557            "uppercase" => {
6558                if evaluated_args.len() != 1 {
6559                    return Err(EvaluatorError::EvaluationError(
6560                        "uppercase() requires exactly 1 argument".to_string(),
6561                    ));
6562                }
6563                if evaluated_args[0].is_undefined() {
6564                    return Ok(JValue::Undefined);
6565                }
6566                match &evaluated_args[0] {
6567                    JValue::String(s) => Ok(functions::string::uppercase(s)?),
6568                    _ => Err(EvaluatorError::TypeError(
6569                        "T0410: Argument 1 of function uppercase does not match function signature"
6570                            .to_string(),
6571                    )),
6572                }
6573            }
6574            "lowercase" => {
6575                if evaluated_args.len() != 1 {
6576                    return Err(EvaluatorError::EvaluationError(
6577                        "lowercase() requires exactly 1 argument".to_string(),
6578                    ));
6579                }
6580                if evaluated_args[0].is_undefined() {
6581                    return Ok(JValue::Undefined);
6582                }
6583                match &evaluated_args[0] {
6584                    JValue::String(s) => Ok(functions::string::lowercase(s)?),
6585                    _ => Err(EvaluatorError::TypeError(
6586                        "T0410: Argument 1 of function lowercase does not match function signature"
6587                            .to_string(),
6588                    )),
6589                }
6590            }
6591            "number" => {
6592                if evaluated_args.is_empty() {
6593                    return Err(EvaluatorError::EvaluationError(
6594                        "number() requires at least 1 argument".to_string(),
6595                    ));
6596                }
6597                if evaluated_args.len() > 1 {
6598                    return Err(EvaluatorError::TypeError(
6599                        "T0410: Argument 2 of function number does not match function signature"
6600                            .to_string(),
6601                    ));
6602                }
6603                if evaluated_args[0].is_undefined() {
6604                    return Ok(JValue::Undefined);
6605                }
6606                Ok(functions::numeric::number(&evaluated_args[0])?)
6607            }
6608            "sum" => {
6609                if evaluated_args.len() != 1 {
6610                    return Err(EvaluatorError::EvaluationError(
6611                        "sum() requires exactly 1 argument".to_string(),
6612                    ));
6613                }
6614                // Return undefined if argument is undefined
6615                if evaluated_args[0].is_undefined() {
6616                    return Ok(JValue::Undefined);
6617                }
6618                match &evaluated_args[0] {
6619                    JValue::Null => Ok(JValue::Null),
6620                    JValue::Array(arr) => {
6621                        // Use zero-clone iterator-based sum
6622                        Ok(aggregation::sum(arr)?)
6623                    }
6624                    // Non-array values: extract number directly
6625                    JValue::Number(n) => Ok(JValue::Number(*n)),
6626                    other => Ok(functions::numeric::sum(&[other.clone()])?),
6627                }
6628            }
6629            "count" => {
6630                if evaluated_args.len() != 1 {
6631                    return Err(EvaluatorError::EvaluationError(
6632                        "count() requires exactly 1 argument".to_string(),
6633                    ));
6634                }
6635                // Return 0 if argument is undefined
6636                if evaluated_args[0].is_undefined() {
6637                    return Ok(JValue::from(0i64));
6638                }
6639                match &evaluated_args[0] {
6640                    JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
6641                    JValue::Array(arr) => Ok(functions::array::count(arr)?),
6642                    _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
6643                }
6644            }
6645            "substring" => {
6646                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6647                    return Err(EvaluatorError::EvaluationError(
6648                        "substring() requires 2 or 3 arguments".to_string(),
6649                    ));
6650                }
6651                if evaluated_args[0].is_undefined() {
6652                    return Ok(JValue::Undefined);
6653                }
6654                match (&evaluated_args[0], &evaluated_args[1]) {
6655                    (JValue::String(s), JValue::Number(start)) => {
6656                        let length = if evaluated_args.len() == 3 {
6657                            match &evaluated_args[2] {
6658                                JValue::Number(l) => Some(*l as i64),
6659                                _ => return Err(EvaluatorError::TypeError(
6660                                    "T0410: Argument 3 of function substring does not match function signature".to_string(),
6661                                )),
6662                            }
6663                        } else {
6664                            None
6665                        };
6666                        Ok(functions::string::substring(s, *start as i64, length)?)
6667                    }
6668                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6669                        "T0410: Argument 2 of function substring does not match function signature"
6670                            .to_string(),
6671                    )),
6672                    _ => Err(EvaluatorError::TypeError(
6673                        "T0410: Argument 1 of function substring does not match function signature"
6674                            .to_string(),
6675                    )),
6676                }
6677            }
6678            "substringBefore" => {
6679                if evaluated_args.len() != 2 {
6680                    return Err(EvaluatorError::TypeError(
6681                        "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
6682                    ));
6683                }
6684                if evaluated_args[0].is_undefined() {
6685                    return Ok(JValue::Undefined);
6686                }
6687                match (&evaluated_args[0], &evaluated_args[1]) {
6688                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
6689                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6690                        "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
6691                    )),
6692                    _ => Err(EvaluatorError::TypeError(
6693                        "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
6694                    )),
6695                }
6696            }
6697            "substringAfter" => {
6698                if evaluated_args.len() != 2 {
6699                    return Err(EvaluatorError::TypeError(
6700                        "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
6701                    ));
6702                }
6703                if evaluated_args[0].is_undefined() {
6704                    return Ok(JValue::Undefined);
6705                }
6706                match (&evaluated_args[0], &evaluated_args[1]) {
6707                    (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
6708                    (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6709                        "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
6710                    )),
6711                    _ => Err(EvaluatorError::TypeError(
6712                        "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
6713                    )),
6714                }
6715            }
6716            "pad" => {
6717                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
6718                    return Err(EvaluatorError::EvaluationError(
6719                        "pad() requires 2 or 3 arguments".to_string(),
6720                    ));
6721                }
6722
6723                // First argument: string to pad
6724                let string = match &evaluated_args[0] {
6725                    JValue::String(s) => s.clone(),
6726                    JValue::Null => return Ok(JValue::Null),
6727                    JValue::Undefined => return Ok(JValue::Undefined),
6728                    _ => {
6729                        return Err(EvaluatorError::TypeError(
6730                            "pad() first argument must be a string".to_string(),
6731                        ))
6732                    }
6733                };
6734
6735                // Second argument: width (negative = left pad, positive = right pad)
6736                let width = match &evaluated_args.get(1) {
6737                    Some(JValue::Number(n)) => *n as i32,
6738                    _ => {
6739                        return Err(EvaluatorError::TypeError(
6740                            "pad() second argument must be a number".to_string(),
6741                        ))
6742                    }
6743                };
6744
6745                // Third argument: padding string (optional, defaults to space)
6746                let pad_string = match evaluated_args.get(2) {
6747                    Some(JValue::String(s)) if !s.is_empty() => s.clone(),
6748                    _ => Rc::from(" "),
6749                };
6750
6751                let abs_width = width.unsigned_abs() as usize;
6752                // Count Unicode characters (code points), not bytes
6753                let char_count = string.chars().count();
6754
6755                if char_count >= abs_width {
6756                    // String is already long enough
6757                    return Ok(JValue::string(string));
6758                }
6759
6760                let padding_needed = abs_width - char_count;
6761
6762                let pad_chars: Vec<char> = pad_string.chars().collect();
6763                let mut padding = String::with_capacity(padding_needed);
6764                for i in 0..padding_needed {
6765                    padding.push(pad_chars[i % pad_chars.len()]);
6766                }
6767
6768                let result = if width < 0 {
6769                    // Left pad (negative width)
6770                    format!("{}{}", padding, string)
6771                } else {
6772                    // Right pad (positive width)
6773                    format!("{}{}", string, padding)
6774                };
6775
6776                Ok(JValue::string(result))
6777            }
6778
6779            "trim" => {
6780                if evaluated_args.is_empty() {
6781                    return Ok(JValue::Null); // undefined
6782                }
6783                if evaluated_args.len() != 1 {
6784                    return Err(EvaluatorError::EvaluationError(
6785                        "trim() requires at most 1 argument".to_string(),
6786                    ));
6787                }
6788                match &evaluated_args[0] {
6789                    JValue::Null => Ok(JValue::Null),
6790                    JValue::String(s) => Ok(functions::string::trim(s)?),
6791                    _ => Err(EvaluatorError::TypeError(
6792                        "trim() requires a string argument".to_string(),
6793                    )),
6794                }
6795            }
6796            "contains" => {
6797                if evaluated_args.len() != 2 {
6798                    return Err(EvaluatorError::EvaluationError(
6799                        "contains() requires exactly 2 arguments".to_string(),
6800                    ));
6801                }
6802                if evaluated_args[0].is_null() {
6803                    return Ok(JValue::Null);
6804                }
6805                if evaluated_args[0].is_undefined() {
6806                    return Ok(JValue::Undefined);
6807                }
6808                match &evaluated_args[0] {
6809                    JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
6810                    _ => Err(EvaluatorError::TypeError(
6811                        "contains() requires a string as the first argument".to_string(),
6812                    )),
6813                }
6814            }
6815            "split" => {
6816                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6817                    return Err(EvaluatorError::EvaluationError(
6818                        "split() requires 2 or 3 arguments".to_string(),
6819                    ));
6820                }
6821                if evaluated_args[0].is_null() {
6822                    return Ok(JValue::Null);
6823                }
6824                if evaluated_args[0].is_undefined() {
6825                    return Ok(JValue::Undefined);
6826                }
6827                match &evaluated_args[0] {
6828                    JValue::String(s) => {
6829                        let limit = if evaluated_args.len() == 3 {
6830                            match &evaluated_args[2] {
6831                                JValue::Number(n) => {
6832                                    let f = *n;
6833                                    // Negative limit is an error
6834                                    if f < 0.0 {
6835                                        return Err(EvaluatorError::EvaluationError(
6836                                            "D3020: Third argument of split function must be a positive number".to_string(),
6837                                        ));
6838                                    }
6839                                    // Floor the value for non-integer limits
6840                                    Some(f.floor() as usize)
6841                                }
6842                                _ => {
6843                                    return Err(EvaluatorError::TypeError(
6844                                        "split() limit must be a number".to_string(),
6845                                    ))
6846                                }
6847                            }
6848                        } else {
6849                            None
6850                        };
6851                        Ok(functions::string::split(s, &evaluated_args[1], limit)?)
6852                    }
6853                    _ => Err(EvaluatorError::TypeError(
6854                        "split() requires a string as the first argument".to_string(),
6855                    )),
6856                }
6857            }
6858            "join" => {
6859                // Special case: if first arg is undefined, return undefined
6860                // But if separator (2nd arg) is undefined, use empty string (default)
6861                if evaluated_args.is_empty() {
6862                    return Err(EvaluatorError::TypeError(
6863                        "T0410: Argument 1 of function $join does not match function signature"
6864                            .to_string(),
6865                    ));
6866                }
6867                if evaluated_args[0].is_null() {
6868                    return Ok(JValue::Null);
6869                }
6870                if evaluated_args[0].is_undefined() {
6871                    return Ok(JValue::Undefined);
6872                }
6873
6874                // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
6875                // The signature handles coercion and validation
6876                use crate::signature::Signature;
6877
6878                let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
6879                    EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
6880                })?;
6881
6882                let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
6883                    Ok(args) => args,
6884                    Err(crate::signature::SignatureError::UndefinedArgument) => {
6885                        // This can happen if the separator is undefined
6886                        // In that case, just validate the first arg and use default separator
6887                        let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
6888                            EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
6889                        })?;
6890
6891                        match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
6892                            Ok(args) => args,
6893                            Err(crate::signature::SignatureError::ArrayTypeMismatch {
6894                                index,
6895                                expected,
6896                            }) => {
6897                                return Err(EvaluatorError::TypeError(format!(
6898                                    "T0412: Argument {} of function $join must be an array of {}",
6899                                    index, expected
6900                                )));
6901                            }
6902                            Err(e) => {
6903                                return Err(EvaluatorError::TypeError(format!(
6904                                    "Signature validation failed: {}",
6905                                    e
6906                                )));
6907                            }
6908                        }
6909                    }
6910                    Err(crate::signature::SignatureError::ArgumentTypeMismatch {
6911                        index,
6912                        expected,
6913                    }) => {
6914                        return Err(EvaluatorError::TypeError(
6915                            format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
6916                        ));
6917                    }
6918                    Err(crate::signature::SignatureError::ArrayTypeMismatch {
6919                        index,
6920                        expected,
6921                    }) => {
6922                        return Err(EvaluatorError::TypeError(format!(
6923                            "T0412: Argument {} of function $join must be an array of {}",
6924                            index, expected
6925                        )));
6926                    }
6927                    Err(e) => {
6928                        return Err(EvaluatorError::TypeError(format!(
6929                            "Signature validation failed: {}",
6930                            e
6931                        )));
6932                    }
6933                };
6934
6935                // After coercion, first arg is guaranteed to be an array of strings
6936                match &coerced_args[0] {
6937                    JValue::Array(arr) => {
6938                        let separator = if coerced_args.len() == 2 {
6939                            match &coerced_args[1] {
6940                                JValue::String(s) => Some(&**s),
6941                                JValue::Null => None, // Undefined separator -> use empty string
6942                                _ => None,            // Signature should have validated this
6943                            }
6944                        } else {
6945                            None // No separator provided -> use empty string
6946                        };
6947                        Ok(functions::string::join(arr, separator)?)
6948                    }
6949                    JValue::Null => Ok(JValue::Null),
6950                    _ => unreachable!("Signature validation should ensure array type"),
6951                }
6952            }
6953            "replace" => {
6954                if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
6955                    return Err(EvaluatorError::EvaluationError(
6956                        "replace() requires 3 or 4 arguments".to_string(),
6957                    ));
6958                }
6959                if evaluated_args[0].is_null() {
6960                    return Ok(JValue::Null);
6961                }
6962                if evaluated_args[0].is_undefined() {
6963                    return Ok(JValue::Undefined);
6964                }
6965
6966                // Check if replacement (3rd arg) is a function/lambda
6967                let replacement_is_lambda = matches!(
6968                    evaluated_args[2],
6969                    JValue::Lambda { .. } | JValue::Builtin { .. }
6970                );
6971
6972                if replacement_is_lambda {
6973                    // Lambda replacement mode
6974                    return self.replace_with_lambda(
6975                        &evaluated_args[0],
6976                        &evaluated_args[1],
6977                        &evaluated_args[2],
6978                        if evaluated_args.len() == 4 {
6979                            Some(&evaluated_args[3])
6980                        } else {
6981                            None
6982                        },
6983                        data,
6984                    );
6985                }
6986
6987                // String replacement mode
6988                match (&evaluated_args[0], &evaluated_args[2]) {
6989                    (JValue::String(s), JValue::String(replacement)) => {
6990                        let limit = if evaluated_args.len() == 4 {
6991                            match &evaluated_args[3] {
6992                                JValue::Number(n) => {
6993                                    let lim_f64 = *n;
6994                                    if lim_f64 < 0.0 {
6995                                        return Err(EvaluatorError::EvaluationError(format!(
6996                                            "D3011: Limit must be non-negative, got {}",
6997                                            lim_f64
6998                                        )));
6999                                    }
7000                                    Some(lim_f64 as usize)
7001                                }
7002                                _ => {
7003                                    return Err(EvaluatorError::TypeError(
7004                                        "replace() limit must be a number".to_string(),
7005                                    ))
7006                                }
7007                            }
7008                        } else {
7009                            None
7010                        };
7011                        Ok(functions::string::replace(
7012                            s,
7013                            &evaluated_args[1],
7014                            replacement,
7015                            limit,
7016                        )?)
7017                    }
7018                    _ => Err(EvaluatorError::TypeError(
7019                        "replace() requires string arguments".to_string(),
7020                    )),
7021                }
7022            }
7023            "match" => {
7024                // $match(str, pattern [, limit])
7025                // Returns array of match objects for regex matches or custom matcher function
7026                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7027                    return Err(EvaluatorError::EvaluationError(
7028                        "match() requires 1 to 3 arguments".to_string(),
7029                    ));
7030                }
7031                if evaluated_args[0].is_null() {
7032                    return Ok(JValue::Null);
7033                }
7034                if evaluated_args[0].is_undefined() {
7035                    return Ok(JValue::Undefined);
7036                }
7037
7038                let s = match &evaluated_args[0] {
7039                    JValue::String(s) => s.clone(),
7040                    _ => {
7041                        return Err(EvaluatorError::TypeError(
7042                            "match() first argument must be a string".to_string(),
7043                        ))
7044                    }
7045                };
7046
7047                // Get optional limit
7048                let limit = if evaluated_args.len() == 3 {
7049                    match &evaluated_args[2] {
7050                        JValue::Number(n) => Some(*n as usize),
7051                        JValue::Null => None,
7052                        _ => {
7053                            return Err(EvaluatorError::TypeError(
7054                                "match() limit must be a number".to_string(),
7055                            ))
7056                        }
7057                    }
7058                } else {
7059                    None
7060                };
7061
7062                // Check if second argument is a custom matcher function (lambda)
7063                let pattern_value = evaluated_args.get(1);
7064                let is_custom_matcher = pattern_value.is_some_and(|val| {
7065                    matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7066                });
7067
7068                if is_custom_matcher {
7069                    // Custom matcher function support
7070                    // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7071                    return self.match_with_custom_matcher(&s, &args[1], limit, data);
7072                }
7073
7074                // Get regex pattern from second argument
7075                let (pattern, flags) = match pattern_value {
7076                    Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7077                        EvaluatorError::TypeError(
7078                            "match() second argument must be a regex pattern or matcher function"
7079                                .to_string(),
7080                        )
7081                    })?,
7082                    None => (".*".to_string(), "".to_string()),
7083                };
7084
7085                // Build regex
7086                let is_global = flags.contains('g');
7087                let regex_pattern = if flags.contains('i') {
7088                    format!("(?i){}", pattern)
7089                } else {
7090                    pattern.clone()
7091                };
7092
7093                let re = regex::Regex::new(&regex_pattern).map_err(|e| {
7094                    EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7095                })?;
7096
7097                let mut results = Vec::new();
7098                let mut count = 0;
7099
7100                for caps in re.captures_iter(&s) {
7101                    if let Some(lim) = limit {
7102                        if count >= lim {
7103                            break;
7104                        }
7105                    }
7106
7107                    let full_match = caps.get(0).unwrap();
7108                    let mut match_obj = IndexMap::new();
7109                    match_obj.insert(
7110                        "match".to_string(),
7111                        JValue::string(full_match.as_str().to_string()),
7112                    );
7113                    match_obj.insert(
7114                        "index".to_string(),
7115                        JValue::Number(full_match.start() as f64),
7116                    );
7117
7118                    // Collect capture groups
7119                    let mut groups: Vec<JValue> = Vec::new();
7120                    for i in 1..caps.len() {
7121                        if let Some(group) = caps.get(i) {
7122                            groups.push(JValue::string(group.as_str().to_string()));
7123                        } else {
7124                            groups.push(JValue::Null);
7125                        }
7126                    }
7127                    if !groups.is_empty() {
7128                        match_obj.insert("groups".to_string(), JValue::array(groups));
7129                    }
7130
7131                    results.push(JValue::object(match_obj));
7132                    count += 1;
7133
7134                    // If not global, only return first match
7135                    if !is_global {
7136                        break;
7137                    }
7138                }
7139
7140                if results.is_empty() {
7141                    Ok(JValue::Null)
7142                } else if results.len() == 1 && !is_global {
7143                    // Single match (non-global) returns the match object directly
7144                    Ok(results.into_iter().next().unwrap())
7145                } else {
7146                    Ok(JValue::array(results))
7147                }
7148            }
7149            "max" => {
7150                if evaluated_args.len() != 1 {
7151                    return Err(EvaluatorError::EvaluationError(
7152                        "max() requires exactly 1 argument".to_string(),
7153                    ));
7154                }
7155                // Check for undefined
7156                if evaluated_args[0].is_undefined() {
7157                    return Ok(JValue::Undefined);
7158                }
7159                match &evaluated_args[0] {
7160                    JValue::Null => Ok(JValue::Null),
7161                    JValue::Array(arr) => {
7162                        // Use zero-clone iterator-based max
7163                        Ok(aggregation::max(arr)?)
7164                    }
7165                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7166                    _ => Err(EvaluatorError::TypeError(
7167                        "max() requires an array or number argument".to_string(),
7168                    )),
7169                }
7170            }
7171            "min" => {
7172                if evaluated_args.len() != 1 {
7173                    return Err(EvaluatorError::EvaluationError(
7174                        "min() requires exactly 1 argument".to_string(),
7175                    ));
7176                }
7177                // Check for undefined
7178                if evaluated_args[0].is_undefined() {
7179                    return Ok(JValue::Undefined);
7180                }
7181                match &evaluated_args[0] {
7182                    JValue::Null => Ok(JValue::Null),
7183                    JValue::Array(arr) => {
7184                        // Use zero-clone iterator-based min
7185                        Ok(aggregation::min(arr)?)
7186                    }
7187                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7188                    _ => Err(EvaluatorError::TypeError(
7189                        "min() requires an array or number argument".to_string(),
7190                    )),
7191                }
7192            }
7193            "average" => {
7194                if evaluated_args.len() != 1 {
7195                    return Err(EvaluatorError::EvaluationError(
7196                        "average() requires exactly 1 argument".to_string(),
7197                    ));
7198                }
7199                // Return undefined if argument is undefined
7200                if evaluated_args[0].is_undefined() {
7201                    return Ok(JValue::Undefined);
7202                }
7203                match &evaluated_args[0] {
7204                    JValue::Null => Ok(JValue::Null),
7205                    JValue::Array(arr) => {
7206                        // Use zero-clone iterator-based average
7207                        Ok(aggregation::average(arr)?)
7208                    }
7209                    JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7210                    _ => Err(EvaluatorError::TypeError(
7211                        "average() requires an array or number argument".to_string(),
7212                    )),
7213                }
7214            }
7215            "abs" => {
7216                if evaluated_args.len() != 1 {
7217                    return Err(EvaluatorError::EvaluationError(
7218                        "abs() requires exactly 1 argument".to_string(),
7219                    ));
7220                }
7221                match &evaluated_args[0] {
7222                    JValue::Null => Ok(JValue::Null),
7223                    JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
7224                    _ => Err(EvaluatorError::TypeError(
7225                        "abs() requires a number argument".to_string(),
7226                    )),
7227                }
7228            }
7229            "floor" => {
7230                if evaluated_args.len() != 1 {
7231                    return Err(EvaluatorError::EvaluationError(
7232                        "floor() requires exactly 1 argument".to_string(),
7233                    ));
7234                }
7235                match &evaluated_args[0] {
7236                    JValue::Null => Ok(JValue::Null),
7237                    JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
7238                    _ => Err(EvaluatorError::TypeError(
7239                        "floor() requires a number argument".to_string(),
7240                    )),
7241                }
7242            }
7243            "ceil" => {
7244                if evaluated_args.len() != 1 {
7245                    return Err(EvaluatorError::EvaluationError(
7246                        "ceil() requires exactly 1 argument".to_string(),
7247                    ));
7248                }
7249                match &evaluated_args[0] {
7250                    JValue::Null => Ok(JValue::Null),
7251                    JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
7252                    _ => Err(EvaluatorError::TypeError(
7253                        "ceil() requires a number argument".to_string(),
7254                    )),
7255                }
7256            }
7257            "round" => {
7258                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7259                    return Err(EvaluatorError::EvaluationError(
7260                        "round() requires 1 or 2 arguments".to_string(),
7261                    ));
7262                }
7263                match &evaluated_args[0] {
7264                    JValue::Null => Ok(JValue::Null),
7265                    JValue::Number(n) => {
7266                        let precision = if evaluated_args.len() == 2 {
7267                            match &evaluated_args[1] {
7268                                JValue::Number(p) => Some(*p as i32),
7269                                _ => {
7270                                    return Err(EvaluatorError::TypeError(
7271                                        "round() precision must be a number".to_string(),
7272                                    ))
7273                                }
7274                            }
7275                        } else {
7276                            None
7277                        };
7278                        Ok(functions::numeric::round(*n, precision)?)
7279                    }
7280                    _ => Err(EvaluatorError::TypeError(
7281                        "round() requires a number argument".to_string(),
7282                    )),
7283                }
7284            }
7285            "sqrt" => {
7286                if evaluated_args.len() != 1 {
7287                    return Err(EvaluatorError::EvaluationError(
7288                        "sqrt() requires exactly 1 argument".to_string(),
7289                    ));
7290                }
7291                match &evaluated_args[0] {
7292                    JValue::Null => Ok(JValue::Null),
7293                    JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
7294                    _ => Err(EvaluatorError::TypeError(
7295                        "sqrt() requires a number argument".to_string(),
7296                    )),
7297                }
7298            }
7299            "power" => {
7300                if evaluated_args.len() != 2 {
7301                    return Err(EvaluatorError::EvaluationError(
7302                        "power() requires exactly 2 arguments".to_string(),
7303                    ));
7304                }
7305                if evaluated_args[0].is_null() {
7306                    return Ok(JValue::Null);
7307                }
7308                if evaluated_args[0].is_undefined() {
7309                    return Ok(JValue::Undefined);
7310                }
7311                match (&evaluated_args[0], &evaluated_args[1]) {
7312                    (JValue::Number(base), JValue::Number(exp)) => {
7313                        Ok(functions::numeric::power(*base, *exp)?)
7314                    }
7315                    _ => Err(EvaluatorError::TypeError(
7316                        "power() requires number arguments".to_string(),
7317                    )),
7318                }
7319            }
7320            "formatNumber" => {
7321                if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7322                    return Err(EvaluatorError::EvaluationError(
7323                        "formatNumber() requires 2 or 3 arguments".to_string(),
7324                    ));
7325                }
7326                if evaluated_args[0].is_null() {
7327                    return Ok(JValue::Null);
7328                }
7329                if evaluated_args[0].is_undefined() {
7330                    return Ok(JValue::Undefined);
7331                }
7332                match (&evaluated_args[0], &evaluated_args[1]) {
7333                    (JValue::Number(num), JValue::String(picture)) => {
7334                        let options = if evaluated_args.len() == 3 {
7335                            Some(&evaluated_args[2])
7336                        } else {
7337                            None
7338                        };
7339                        Ok(functions::numeric::format_number(*num, picture, options)?)
7340                    }
7341                    _ => Err(EvaluatorError::TypeError(
7342                        "formatNumber() requires a number and a string".to_string(),
7343                    )),
7344                }
7345            }
7346            "formatBase" => {
7347                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7348                    return Err(EvaluatorError::EvaluationError(
7349                        "formatBase() requires 1 or 2 arguments".to_string(),
7350                    ));
7351                }
7352                // Handle undefined input
7353                if evaluated_args[0].is_null() {
7354                    return Ok(JValue::Null);
7355                }
7356                if evaluated_args[0].is_undefined() {
7357                    return Ok(JValue::Undefined);
7358                }
7359                match &evaluated_args[0] {
7360                    JValue::Number(num) => {
7361                        let radix = if evaluated_args.len() == 2 {
7362                            match &evaluated_args[1] {
7363                                JValue::Number(r) => Some(r.trunc() as i64),
7364                                _ => {
7365                                    return Err(EvaluatorError::TypeError(
7366                                        "formatBase() radix must be a number".to_string(),
7367                                    ))
7368                                }
7369                            }
7370                        } else {
7371                            None
7372                        };
7373                        Ok(functions::numeric::format_base(*num, radix)?)
7374                    }
7375                    _ => Err(EvaluatorError::TypeError(
7376                        "formatBase() requires a number".to_string(),
7377                    )),
7378                }
7379            }
7380            "formatInteger" => {
7381                if evaluated_args.len() != 2 {
7382                    return Err(EvaluatorError::EvaluationError(
7383                        "formatInteger() requires exactly 2 arguments".to_string(),
7384                    ));
7385                }
7386                match (&evaluated_args[0], &evaluated_args[1]) {
7387                    (JValue::Number(n), JValue::String(picture)) => {
7388                        Ok(crate::datetime::format_integer(*n, picture)?)
7389                    }
7390                    (JValue::Null, _) => Ok(JValue::Null),
7391                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7392                    _ => Err(EvaluatorError::TypeError(
7393                        "formatInteger() requires a number and a string".to_string(),
7394                    )),
7395                }
7396            }
7397            "parseInteger" => {
7398                if evaluated_args.len() != 2 {
7399                    return Err(EvaluatorError::EvaluationError(
7400                        "parseInteger() requires exactly 2 arguments".to_string(),
7401                    ));
7402                }
7403                match (&evaluated_args[0], &evaluated_args[1]) {
7404                    (JValue::String(value), JValue::String(picture)) => {
7405                        Ok(crate::datetime::parse_integer(value, picture)?)
7406                    }
7407                    (JValue::Null, _) => Ok(JValue::Null),
7408                    (JValue::Undefined, _) => Ok(JValue::Undefined),
7409                    _ => Err(EvaluatorError::TypeError(
7410                        "parseInteger() requires a string and a string".to_string(),
7411                    )),
7412                }
7413            }
7414            "append" => {
7415                if evaluated_args.len() != 2 {
7416                    return Err(EvaluatorError::EvaluationError(
7417                        "append() requires exactly 2 arguments".to_string(),
7418                    ));
7419                }
7420                // Handle null/undefined arguments
7421                let first = &evaluated_args[0];
7422                let second = &evaluated_args[1];
7423
7424                // If second arg is null/undefined, return first as-is (no change)
7425                if second.is_null() || second.is_undefined() {
7426                    return Ok(first.clone());
7427                }
7428
7429                // If first arg is null/undefined, return second as-is (appending to nothing gives second)
7430                if first.is_null() || first.is_undefined() {
7431                    return Ok(second.clone());
7432                }
7433
7434                // Convert both to arrays if needed, then append
7435                let arr = match first {
7436                    JValue::Array(a) => a.to_vec(),
7437                    other => vec![other.clone()], // Wrap non-array in array
7438                };
7439
7440                Ok(functions::array::append(&arr, second)?)
7441            }
7442            "reverse" => {
7443                if evaluated_args.len() != 1 {
7444                    return Err(EvaluatorError::EvaluationError(
7445                        "reverse() requires exactly 1 argument".to_string(),
7446                    ));
7447                }
7448                match &evaluated_args[0] {
7449                    JValue::Null => Ok(JValue::Null),
7450                    JValue::Undefined => Ok(JValue::Undefined),
7451                    JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
7452                    _ => Err(EvaluatorError::TypeError(
7453                        "reverse() requires an array argument".to_string(),
7454                    )),
7455                }
7456            }
7457            "shuffle" => {
7458                if evaluated_args.len() != 1 {
7459                    return Err(EvaluatorError::EvaluationError(
7460                        "shuffle() requires exactly 1 argument".to_string(),
7461                    ));
7462                }
7463                if evaluated_args[0].is_null() {
7464                    return Ok(JValue::Null);
7465                }
7466                if evaluated_args[0].is_undefined() {
7467                    return Ok(JValue::Undefined);
7468                }
7469                match &evaluated_args[0] {
7470                    JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
7471                    _ => Err(EvaluatorError::TypeError(
7472                        "shuffle() requires an array argument".to_string(),
7473                    )),
7474                }
7475            }
7476
7477            "sift" => {
7478                // $sift(object, function) or $sift(function) - filter object by predicate
7479                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7480                    return Err(EvaluatorError::EvaluationError(
7481                        "sift() requires 1 or 2 arguments".to_string(),
7482                    ));
7483                }
7484
7485                // Determine which argument is the function
7486                let func_arg = if evaluated_args.len() == 1 {
7487                    &args[0]
7488                } else {
7489                    &args[1]
7490                };
7491
7492                // Detect how many parameters the callback expects
7493                let param_count = self.get_callback_param_count(func_arg);
7494
7495                // Helper function to sift a single object
7496                let sift_object = |evaluator: &mut Self,
7497                                   obj: &IndexMap<String, JValue>,
7498                                   func_node: &AstNode,
7499                                   context_data: &JValue,
7500                                   param_count: usize|
7501                 -> Result<JValue, EvaluatorError> {
7502                    // Only create the object value if callback uses 3 parameters
7503                    let obj_value = if param_count >= 3 {
7504                        Some(JValue::object(obj.clone()))
7505                    } else {
7506                        None
7507                    };
7508
7509                    let mut result = IndexMap::new();
7510                    for (key, value) in obj.iter() {
7511                        // Build argument list based on what callback expects
7512                        let call_args = match param_count {
7513                            1 => vec![value.clone()],
7514                            2 => vec![value.clone(), JValue::string(key.clone())],
7515                            _ => vec![
7516                                value.clone(),
7517                                JValue::string(key.clone()),
7518                                obj_value.as_ref().unwrap().clone(),
7519                            ],
7520                        };
7521
7522                        let pred_result =
7523                            evaluator.apply_function(func_node, &call_args, context_data)?;
7524                        if evaluator.is_truthy(&pred_result) {
7525                            result.insert(key.clone(), value.clone());
7526                        }
7527                    }
7528                    // Return undefined for empty results (will be filtered by function application)
7529                    if result.is_empty() {
7530                        Ok(JValue::Undefined)
7531                    } else {
7532                        Ok(JValue::object(result))
7533                    }
7534                };
7535
7536                // Handle partial application - if only 1 arg, use current context as object
7537                if evaluated_args.len() == 1 {
7538                    // $sift(function) - use current context data as object
7539                    match data {
7540                        JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
7541                        JValue::Array(arr) => {
7542                            // Map sift over each object in the array
7543                            let mut results = Vec::new();
7544                            for item in arr.iter() {
7545                                if let JValue::Object(o) = item {
7546                                    let sifted = sift_object(self, o, &args[0], item, param_count)?;
7547                                    // sift_object returns undefined for empty results
7548                                    if !sifted.is_undefined() {
7549                                        results.push(sifted);
7550                                    }
7551                                }
7552                            }
7553                            Ok(JValue::array(results))
7554                        }
7555                        JValue::Null => Ok(JValue::Null),
7556                        _ => Ok(JValue::Undefined),
7557                    }
7558                } else {
7559                    // $sift(object, function)
7560                    match &evaluated_args[0] {
7561                        JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
7562                        JValue::Null => Ok(JValue::Null),
7563                        _ => Err(EvaluatorError::TypeError(
7564                            "sift() first argument must be an object".to_string(),
7565                        )),
7566                    }
7567                }
7568            }
7569
7570            "zip" => {
7571                if evaluated_args.is_empty() {
7572                    return Err(EvaluatorError::EvaluationError(
7573                        "zip() requires at least 1 argument".to_string(),
7574                    ));
7575                }
7576
7577                // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
7578                // If any argument is null/undefined, return empty array
7579                let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
7580                for arg in &evaluated_args {
7581                    match arg {
7582                        JValue::Array(arr) => {
7583                            if arr.is_empty() {
7584                                // Empty array means result is empty
7585                                return Ok(JValue::array(vec![]));
7586                            }
7587                            arrays.push(arr.to_vec());
7588                        }
7589                        JValue::Null | JValue::Undefined => {
7590                            // Null/undefined means result is empty
7591                            return Ok(JValue::array(vec![]));
7592                        }
7593                        other => {
7594                            // Wrap non-array values in single-element array
7595                            arrays.push(vec![other.clone()]);
7596                        }
7597                    }
7598                }
7599
7600                if arrays.is_empty() {
7601                    return Ok(JValue::array(vec![]));
7602                }
7603
7604                // Find the length of the shortest array
7605                let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
7606
7607                // Zip the arrays together
7608                let mut result = Vec::with_capacity(min_len);
7609                for i in 0..min_len {
7610                    let mut tuple = Vec::with_capacity(arrays.len());
7611                    for array in &arrays {
7612                        tuple.push(array[i].clone());
7613                    }
7614                    result.push(JValue::array(tuple));
7615                }
7616
7617                Ok(JValue::array(result))
7618            }
7619
7620            "sort" => {
7621                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7622                    return Err(EvaluatorError::EvaluationError(
7623                        "sort() requires 1 or 2 arguments".to_string(),
7624                    ));
7625                }
7626
7627                // Use pre-evaluated first argument (avoid double evaluation)
7628                let array_value = &evaluated_args[0];
7629
7630                // Handle undefined input
7631                if array_value.is_null() {
7632                    return Ok(JValue::Null);
7633                }
7634                if array_value.is_undefined() {
7635                    return Ok(JValue::Undefined);
7636                }
7637
7638                let mut arr = match array_value {
7639                    JValue::Array(arr) => arr.to_vec(),
7640                    other => vec![other.clone()],
7641                };
7642
7643                if args.len() == 2 {
7644                    // Sort using the comparator from raw args (need unevaluated lambda AST)
7645                    // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
7646                    self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
7647                    Ok(JValue::array(arr))
7648                } else {
7649                    // Default sort (no comparator)
7650                    Ok(functions::array::sort(&arr)?)
7651                }
7652            }
7653            "distinct" => {
7654                if evaluated_args.len() != 1 {
7655                    return Err(EvaluatorError::EvaluationError(
7656                        "distinct() requires exactly 1 argument".to_string(),
7657                    ));
7658                }
7659                match &evaluated_args[0] {
7660                    JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
7661                    // Non-array input, and arrays of length <= 1, pass through
7662                    // unchanged (jsonata-js functions.js:
7663                    // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
7664                    other => Ok(other.clone()),
7665                }
7666            }
7667            "exists" => {
7668                if evaluated_args.len() != 1 {
7669                    return Err(EvaluatorError::EvaluationError(
7670                        "exists() requires exactly 1 argument".to_string(),
7671                    ));
7672                }
7673                Ok(functions::array::exists(&evaluated_args[0])?)
7674            }
7675            "keys" => {
7676                if evaluated_args.len() != 1 {
7677                    return Err(EvaluatorError::EvaluationError(
7678                        "keys() requires exactly 1 argument".to_string(),
7679                    ));
7680                }
7681
7682                // Helper to unwrap single-element arrays
7683                let unwrap_single = |keys: Vec<JValue>| -> JValue {
7684                    if keys.len() == 1 {
7685                        keys.into_iter().next().unwrap()
7686                    } else {
7687                        JValue::array(keys)
7688                    }
7689                };
7690
7691                match &evaluated_args[0] {
7692                    JValue::Null => Ok(JValue::Null),
7693                    JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
7694                    JValue::Object(obj) => {
7695                        // Return undefined for empty objects
7696                        if obj.is_empty() {
7697                            Ok(JValue::Null)
7698                        } else {
7699                            let keys: Vec<JValue> =
7700                                obj.keys().map(|k| JValue::string(k.clone())).collect();
7701                            Ok(unwrap_single(keys))
7702                        }
7703                    }
7704                    JValue::Array(arr) => {
7705                        // For arrays, collect keys from all objects
7706                        let mut all_keys = Vec::new();
7707                        for item in arr.iter() {
7708                            // Skip lambda/builtin values
7709                            if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
7710                                continue;
7711                            }
7712                            if let JValue::Object(obj) = item {
7713                                for key in obj.keys() {
7714                                    if !all_keys.contains(&JValue::string(key.clone())) {
7715                                        all_keys.push(JValue::string(key.clone()));
7716                                    }
7717                                }
7718                            }
7719                        }
7720                        if all_keys.is_empty() {
7721                            Ok(JValue::Null)
7722                        } else {
7723                            Ok(unwrap_single(all_keys))
7724                        }
7725                    }
7726                    // Non-object types return undefined
7727                    _ => Ok(JValue::Null),
7728                }
7729            }
7730            "lookup" => {
7731                if evaluated_args.len() != 2 {
7732                    return Err(EvaluatorError::EvaluationError(
7733                        "lookup() requires exactly 2 arguments".to_string(),
7734                    ));
7735                }
7736                if evaluated_args[0].is_null() {
7737                    return Ok(JValue::Null);
7738                }
7739                if evaluated_args[0].is_undefined() {
7740                    return Ok(JValue::Undefined);
7741                }
7742
7743                let key = match &evaluated_args[1] {
7744                    JValue::String(k) => &**k,
7745                    _ => {
7746                        return Err(EvaluatorError::TypeError(
7747                            "lookup() requires a string key".to_string(),
7748                        ))
7749                    }
7750                };
7751
7752                // Helper function to recursively lookup in values
7753                fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
7754                    match val {
7755                        JValue::Array(arr) => {
7756                            let mut results = Vec::new();
7757                            for item in arr.iter() {
7758                                let nested = lookup_recursive(item, key);
7759                                results.extend(nested.iter().cloned());
7760                            }
7761                            results
7762                        }
7763                        JValue::Object(obj) => {
7764                            if let Some(v) = obj.get(key) {
7765                                vec![v.clone()]
7766                            } else {
7767                                vec![]
7768                            }
7769                        }
7770                        _ => vec![],
7771                    }
7772                }
7773
7774                let results = lookup_recursive(&evaluated_args[0], key);
7775                if results.is_empty() {
7776                    Ok(JValue::Null)
7777                } else if results.len() == 1 {
7778                    Ok(results[0].clone())
7779                } else {
7780                    Ok(JValue::array(results))
7781                }
7782            }
7783            "spread" => {
7784                if evaluated_args.len() != 1 {
7785                    return Err(EvaluatorError::EvaluationError(
7786                        "spread() requires exactly 1 argument".to_string(),
7787                    ));
7788                }
7789                match &evaluated_args[0] {
7790                    JValue::Null => Ok(JValue::Null),
7791                    // Not a container - pass through unchanged (e.g. so $string() still
7792                    // sees the function value and applies its own function->"" rule).
7793                    lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
7794                    JValue::Object(obj) => Ok(functions::object::spread(obj)?),
7795                    JValue::Array(arr) => {
7796                        // Spread each object in the array
7797                        let mut result = Vec::new();
7798                        for item in arr.iter() {
7799                            match item {
7800                                JValue::Lambda { .. } | JValue::Builtin { .. } => {
7801                                    // Skip lambdas in array
7802                                    continue;
7803                                }
7804                                JValue::Object(obj) => {
7805                                    let spread_result = functions::object::spread(obj)?;
7806                                    if let JValue::Array(spread_items) = spread_result {
7807                                        result.extend(spread_items.iter().cloned());
7808                                    } else {
7809                                        result.push(spread_result);
7810                                    }
7811                                }
7812                                // Non-objects in array are returned unchanged
7813                                other => result.push(other.clone()),
7814                            }
7815                        }
7816                        Ok(JValue::array(result))
7817                    }
7818                    // Non-objects are returned unchanged
7819                    other => Ok(other.clone()),
7820                }
7821            }
7822            "merge" => {
7823                if evaluated_args.is_empty() {
7824                    return Err(EvaluatorError::EvaluationError(
7825                        "merge() requires at least 1 argument".to_string(),
7826                    ));
7827                }
7828                // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
7829                // vs multiple object arguments: $merge(obj1, obj2)
7830                if evaluated_args.len() == 1 {
7831                    match &evaluated_args[0] {
7832                        JValue::Array(arr) => Ok(functions::object::merge(arr)?),
7833                        JValue::Null => Ok(JValue::Null),
7834                        JValue::Undefined => Ok(JValue::Undefined),
7835                        JValue::Object(_) => {
7836                            // Single object - just return it
7837                            Ok(evaluated_args[0].clone())
7838                        }
7839                        _ => Err(EvaluatorError::TypeError(
7840                            "merge() requires objects or an array of objects".to_string(),
7841                        )),
7842                    }
7843                } else {
7844                    Ok(functions::object::merge(&evaluated_args)?)
7845                }
7846            }
7847
7848            "map" => {
7849                if args.len() != 2 {
7850                    return Err(EvaluatorError::EvaluationError(
7851                        "map() requires exactly 2 arguments".to_string(),
7852                    ));
7853                }
7854
7855                // Evaluate the array argument
7856                let array = self.evaluate_internal(&args[0], data)?;
7857
7858                match array {
7859                    JValue::Array(arr) => {
7860                        // Detect how many parameters the callback expects
7861                        let param_count = self.get_callback_param_count(&args[1]);
7862
7863                        // CompiledExpr fast path: direct lambda with 1 param, compilable body
7864                        if param_count == 1 {
7865                            if let AstNode::Lambda {
7866                                params,
7867                                body,
7868                                signature: None,
7869                                thunk: false,
7870                            } = &args[1]
7871                            {
7872                                let var_refs: Vec<&str> =
7873                                    params.iter().map(|s| s.as_str()).collect();
7874                                if let Some(compiled) =
7875                                    try_compile_expr_with_allowed_vars(body, &var_refs)
7876                                {
7877                                    let param_name = params[0].as_str();
7878                                    let mut result = Vec::with_capacity(arr.len());
7879                                    let mut vars = HashMap::new();
7880                                    for item in arr.iter() {
7881                                        vars.insert(param_name, item);
7882                                        let mapped = eval_compiled(&compiled, data, Some(&vars))?;
7883                                        if !mapped.is_undefined() {
7884                                            result.push(mapped);
7885                                        }
7886                                    }
7887                                    return Ok(JValue::array(result));
7888                                }
7889                            }
7890                            // Stored lambda variable fast path: $var with pre-compiled body
7891                            if let AstNode::Variable(var_name) = &args[1] {
7892                                if let Some(stored) = self.context.lookup_lambda(var_name) {
7893                                    if let Some(ref ce) = stored.compiled_body.clone() {
7894                                        let param_name = stored.params[0].clone();
7895                                        let captured_data = stored.captured_data.clone();
7896                                        let captured_env_clone = stored.captured_env.clone();
7897                                        let ce_clone = ce.clone();
7898                                        if !captured_env_clone.values().any(|v| {
7899                                            matches!(
7900                                                v,
7901                                                JValue::Lambda { .. } | JValue::Builtin { .. }
7902                                            )
7903                                        }) {
7904                                            let call_data = captured_data.as_ref().unwrap_or(data);
7905                                            let mut result = Vec::with_capacity(arr.len());
7906                                            let mut vars: HashMap<&str, &JValue> =
7907                                                captured_env_clone
7908                                                    .iter()
7909                                                    .map(|(k, v)| (k.as_str(), v))
7910                                                    .collect();
7911                                            for item in arr.iter() {
7912                                                vars.insert(param_name.as_str(), item);
7913                                                let mapped = eval_compiled(
7914                                                    &ce_clone,
7915                                                    call_data,
7916                                                    Some(&vars),
7917                                                )?;
7918                                                if !mapped.is_undefined() {
7919                                                    result.push(mapped);
7920                                                }
7921                                            }
7922                                            return Ok(JValue::array(result));
7923                                        }
7924                                    }
7925                                }
7926                            }
7927                        }
7928
7929                        // Only create the array value if callback uses 3 parameters
7930                        let arr_value = if param_count >= 3 {
7931                            Some(JValue::Array(arr.clone()))
7932                        } else {
7933                            None
7934                        };
7935
7936                        let mut result = Vec::with_capacity(arr.len());
7937                        for (index, item) in arr.iter().enumerate() {
7938                            // Build argument list based on what callback expects
7939                            let call_args = match param_count {
7940                                1 => vec![item.clone()],
7941                                2 => vec![item.clone(), JValue::Number(index as f64)],
7942                                _ => vec![
7943                                    item.clone(),
7944                                    JValue::Number(index as f64),
7945                                    arr_value.as_ref().unwrap().clone(),
7946                                ],
7947                            };
7948
7949                            let mapped = self.apply_function(&args[1], &call_args, data)?;
7950                            // Filter out undefined results but keep explicit null (JSONata map semantics)
7951                            // undefined comes from missing else clause, null is explicit
7952                            if !mapped.is_undefined() {
7953                                result.push(mapped);
7954                            }
7955                        }
7956                        Ok(JValue::array(result))
7957                    }
7958                    JValue::Null => Ok(JValue::Null),
7959                    JValue::Undefined => Ok(JValue::Undefined),
7960                    _ => Err(EvaluatorError::TypeError(
7961                        "map() first argument must be an array".to_string(),
7962                    )),
7963                }
7964            }
7965
7966            "filter" => {
7967                if args.len() != 2 {
7968                    return Err(EvaluatorError::EvaluationError(
7969                        "filter() requires exactly 2 arguments".to_string(),
7970                    ));
7971                }
7972
7973                // Evaluate the array argument
7974                let array = self.evaluate_internal(&args[0], data)?;
7975
7976                // Handle undefined input - return undefined
7977                if array.is_undefined() {
7978                    return Ok(JValue::Undefined);
7979                }
7980
7981                // Handle null input
7982                if array.is_null() {
7983                    return Ok(JValue::Undefined);
7984                }
7985
7986                // Coerce non-array values to single-element arrays
7987                // Track if input was a single value to unwrap result appropriately
7988                // Use references to avoid upfront cloning of all elements
7989                let single_holder;
7990                let (items, was_single_value): (&[JValue], bool) = match &array {
7991                    JValue::Array(arr) => (arr.as_slice(), false),
7992                    _ => {
7993                        single_holder = [array];
7994                        (&single_holder[..], true)
7995                    }
7996                };
7997
7998                // Detect how many parameters the callback expects
7999                let param_count = self.get_callback_param_count(&args[1]);
8000
8001                // CompiledExpr fast path: direct lambda with 1 param, compilable body
8002                if param_count == 1 {
8003                    if let AstNode::Lambda {
8004                        params,
8005                        body,
8006                        signature: None,
8007                        thunk: false,
8008                    } = &args[1]
8009                    {
8010                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8011                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8012                        {
8013                            let param_name = params[0].as_str();
8014                            let mut result = Vec::with_capacity(items.len() / 2);
8015                            let mut vars = HashMap::new();
8016                            for item in items.iter() {
8017                                vars.insert(param_name, item);
8018                                let pred_result = eval_compiled(&compiled, data, Some(&vars))?;
8019                                if compiled_is_truthy(&pred_result) {
8020                                    result.push(item.clone());
8021                                }
8022                            }
8023                            if was_single_value {
8024                                if result.len() == 1 {
8025                                    return Ok(result.remove(0));
8026                                } else if result.is_empty() {
8027                                    return Ok(JValue::Undefined);
8028                                }
8029                            }
8030                            return Ok(JValue::array(result));
8031                        }
8032                    }
8033                    // Stored lambda variable fast path: $var with pre-compiled body
8034                    if let AstNode::Variable(var_name) = &args[1] {
8035                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8036                            if let Some(ref ce) = stored.compiled_body.clone() {
8037                                let param_name = stored.params[0].clone();
8038                                let captured_data = stored.captured_data.clone();
8039                                let captured_env_clone = stored.captured_env.clone();
8040                                let ce_clone = ce.clone();
8041                                if !captured_env_clone.values().any(|v| {
8042                                    matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8043                                }) {
8044                                    let call_data = captured_data.as_ref().unwrap_or(data);
8045                                    let mut result = Vec::with_capacity(items.len() / 2);
8046                                    let mut vars: HashMap<&str, &JValue> = captured_env_clone
8047                                        .iter()
8048                                        .map(|(k, v)| (k.as_str(), v))
8049                                        .collect();
8050                                    for item in items.iter() {
8051                                        vars.insert(param_name.as_str(), item);
8052                                        let pred_result =
8053                                            eval_compiled(&ce_clone, call_data, Some(&vars))?;
8054                                        if compiled_is_truthy(&pred_result) {
8055                                            result.push(item.clone());
8056                                        }
8057                                    }
8058                                    if was_single_value {
8059                                        if result.len() == 1 {
8060                                            return Ok(result.remove(0));
8061                                        } else if result.is_empty() {
8062                                            return Ok(JValue::Undefined);
8063                                        }
8064                                    }
8065                                    return Ok(JValue::array(result));
8066                                }
8067                            }
8068                        }
8069                    }
8070                }
8071
8072                // Only create the array value if callback uses 3 parameters
8073                let arr_value = if param_count >= 3 {
8074                    Some(JValue::array(items.to_vec()))
8075                } else {
8076                    None
8077                };
8078
8079                let mut result = Vec::with_capacity(items.len() / 2);
8080
8081                for (index, item) in items.iter().enumerate() {
8082                    // Build argument list based on what callback expects
8083                    let call_args = match param_count {
8084                        1 => vec![item.clone()],
8085                        2 => vec![item.clone(), JValue::Number(index as f64)],
8086                        _ => vec![
8087                            item.clone(),
8088                            JValue::Number(index as f64),
8089                            arr_value.as_ref().unwrap().clone(),
8090                        ],
8091                    };
8092
8093                    let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8094                    if self.is_truthy(&predicate_result) {
8095                        result.push(item.clone());
8096                    }
8097                }
8098
8099                // If input was a single value, return the single matching item
8100                // (or undefined if no match)
8101                if was_single_value {
8102                    if result.len() == 1 {
8103                        return Ok(result.remove(0));
8104                    } else if result.is_empty() {
8105                        return Ok(JValue::Undefined);
8106                    }
8107                }
8108
8109                Ok(JValue::array(result))
8110            }
8111
8112            "reduce" => {
8113                if args.len() < 2 || args.len() > 3 {
8114                    return Err(EvaluatorError::EvaluationError(
8115                        "reduce() requires 2 or 3 arguments".to_string(),
8116                    ));
8117                }
8118
8119                // Check that the callback function has at least 2 parameters
8120                if let AstNode::Lambda { params, .. } = &args[1] {
8121                    if params.len() < 2 {
8122                        return Err(EvaluatorError::EvaluationError(
8123                            "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
8124                        ));
8125                    }
8126                } else if let AstNode::Function { name, .. } = &args[1] {
8127                    // For now, we can't validate built-in function signatures here
8128                    // But user-defined functions via lambda will be validated above
8129                    let _ = name; // avoid unused warning
8130                }
8131
8132                // Evaluate the array argument
8133                let array = self.evaluate_internal(&args[0], data)?;
8134
8135                // Convert single value to array (JSONata reduce accepts single values)
8136                // Use references to avoid upfront cloning of all elements
8137                let single_holder;
8138                let items: &[JValue] = match &array {
8139                    JValue::Array(arr) => arr.as_slice(),
8140                    JValue::Null => return Ok(JValue::Null),
8141                    _ => {
8142                        single_holder = [array];
8143                        &single_holder[..]
8144                    }
8145                };
8146
8147                if items.is_empty() {
8148                    // Return initial value if provided, otherwise null
8149                    return if args.len() == 3 {
8150                        self.evaluate_internal(&args[2], data)
8151                    } else {
8152                        Ok(JValue::Null)
8153                    };
8154                }
8155
8156                // Get initial accumulator
8157                let mut accumulator = if args.len() == 3 {
8158                    self.evaluate_internal(&args[2], data)?
8159                } else {
8160                    items[0].clone()
8161                };
8162
8163                let start_idx = if args.len() == 3 { 0 } else { 1 };
8164
8165                // Detect how many parameters the callback expects
8166                let param_count = self.get_callback_param_count(&args[1]);
8167
8168                // CompiledExpr fast path: direct lambda with 2 params, compilable body
8169                if param_count == 2 {
8170                    if let AstNode::Lambda {
8171                        params,
8172                        body,
8173                        signature: None,
8174                        thunk: false,
8175                    } = &args[1]
8176                    {
8177                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8178                        if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8179                        {
8180                            let acc_name = params[0].as_str();
8181                            let item_name = params[1].as_str();
8182                            for item in items[start_idx..].iter() {
8183                                let vars: HashMap<&str, &JValue> =
8184                                    HashMap::from([(acc_name, &accumulator), (item_name, item)]);
8185                                accumulator = eval_compiled(&compiled, data, Some(&vars))?;
8186                            }
8187                            return Ok(accumulator);
8188                        }
8189                    }
8190                    // Stored lambda variable fast path: $var with pre-compiled body
8191                    if let AstNode::Variable(var_name) = &args[1] {
8192                        if let Some(stored) = self.context.lookup_lambda(var_name) {
8193                            if stored.params.len() == 2 {
8194                                if let Some(ref ce) = stored.compiled_body.clone() {
8195                                    let acc_param = stored.params[0].clone();
8196                                    let item_param = stored.params[1].clone();
8197                                    let captured_data = stored.captured_data.clone();
8198                                    let captured_env_clone = stored.captured_env.clone();
8199                                    let ce_clone = ce.clone();
8200                                    if !captured_env_clone.values().any(|v| {
8201                                        matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8202                                    }) {
8203                                        let call_data = captured_data.as_ref().unwrap_or(data);
8204                                        for item in items[start_idx..].iter() {
8205                                            let mut vars: HashMap<&str, &JValue> =
8206                                                captured_env_clone
8207                                                    .iter()
8208                                                    .map(|(k, v)| (k.as_str(), v))
8209                                                    .collect();
8210                                            vars.insert(acc_param.as_str(), &accumulator);
8211                                            vars.insert(item_param.as_str(), item);
8212                                            // Evaluate and drop vars before assigning accumulator
8213                                            // to satisfy borrow checker (vars borrows accumulator)
8214                                            let new_acc =
8215                                                eval_compiled(&ce_clone, call_data, Some(&vars))?;
8216                                            drop(vars);
8217                                            accumulator = new_acc;
8218                                        }
8219                                        return Ok(accumulator);
8220                                    }
8221                                }
8222                            }
8223                        }
8224                    }
8225                }
8226
8227                // Only create the array value if callback uses 4 parameters
8228                let arr_value = if param_count >= 4 {
8229                    Some(JValue::array(items.to_vec()))
8230                } else {
8231                    None
8232                };
8233
8234                // Apply function to each element
8235                for (idx, item) in items[start_idx..].iter().enumerate() {
8236                    // For reduce, the function receives (accumulator, value, index, array)
8237                    // Callbacks may use any subset of these parameters
8238                    let actual_idx = start_idx + idx;
8239
8240                    // Build argument list based on what callback expects
8241                    let call_args = match param_count {
8242                        2 => vec![accumulator.clone(), item.clone()],
8243                        3 => vec![
8244                            accumulator.clone(),
8245                            item.clone(),
8246                            JValue::Number(actual_idx as f64),
8247                        ],
8248                        _ => vec![
8249                            accumulator.clone(),
8250                            item.clone(),
8251                            JValue::Number(actual_idx as f64),
8252                            arr_value.as_ref().unwrap().clone(),
8253                        ],
8254                    };
8255
8256                    accumulator = self.apply_function(&args[1], &call_args, data)?;
8257                }
8258
8259                Ok(accumulator)
8260            }
8261
8262            "single" => {
8263                if args.is_empty() || args.len() > 2 {
8264                    return Err(EvaluatorError::EvaluationError(
8265                        "single() requires 1 or 2 arguments".to_string(),
8266                    ));
8267                }
8268
8269                // Evaluate the array argument
8270                let array = self.evaluate_internal(&args[0], data)?;
8271
8272                // Convert to array (wrap single values)
8273                let arr = match array {
8274                    JValue::Array(arr) => arr.to_vec(),
8275                    JValue::Null => return Ok(JValue::Null),
8276                    other => vec![other],
8277                };
8278
8279                if args.len() == 1 {
8280                    // No predicate - array must have exactly 1 element
8281                    match arr.len() {
8282                        0 => Err(EvaluatorError::EvaluationError(
8283                            "single() argument is empty".to_string(),
8284                        )),
8285                        1 => Ok(arr.into_iter().next().unwrap()),
8286                        count => Err(EvaluatorError::EvaluationError(format!(
8287                            "single() argument has {} values (expected exactly 1)",
8288                            count
8289                        ))),
8290                    }
8291                } else {
8292                    // With predicate - find exactly 1 matching element
8293                    let arr_value = JValue::array(arr.clone());
8294                    let mut matches = Vec::new();
8295                    for (index, item) in arr.into_iter().enumerate() {
8296                        // Apply predicate function with (item, index, array)
8297                        let predicate_result = self.apply_function(
8298                            &args[1],
8299                            &[
8300                                item.clone(),
8301                                JValue::Number(index as f64),
8302                                arr_value.clone(),
8303                            ],
8304                            data,
8305                        )?;
8306                        if self.is_truthy(&predicate_result) {
8307                            matches.push(item);
8308                        }
8309                    }
8310
8311                    match matches.len() {
8312                        0 => Err(EvaluatorError::EvaluationError(
8313                            "single() predicate matches no values".to_string(),
8314                        )),
8315                        1 => Ok(matches.into_iter().next().unwrap()),
8316                        count => Err(EvaluatorError::EvaluationError(format!(
8317                            "single() predicate matches {} values (expected exactly 1)",
8318                            count
8319                        ))),
8320                    }
8321                }
8322            }
8323
8324            "each" => {
8325                // $each(object, function) - iterate over object, applying function to each value/key pair
8326                // Returns an array of the function results
8327                if args.is_empty() || args.len() > 2 {
8328                    return Err(EvaluatorError::EvaluationError(
8329                        "each() requires 1 or 2 arguments".to_string(),
8330                    ));
8331                }
8332
8333                // Determine which argument is the object and which is the function
8334                let (obj_value, func_arg) = if args.len() == 1 {
8335                    // Single argument: use current data as object
8336                    (data.clone(), &args[0])
8337                } else {
8338                    // Two arguments: first is object, second is function
8339                    (self.evaluate_internal(&args[0], data)?, &args[1])
8340                };
8341
8342                // Detect how many parameters the callback expects
8343                let param_count = self.get_callback_param_count(func_arg);
8344
8345                match obj_value {
8346                    JValue::Object(obj) => {
8347                        let mut result = Vec::new();
8348                        for (key, value) in obj.iter() {
8349                            // Build argument list based on what callback expects
8350                            // The callback receives the value as the first argument and key as second
8351                            let call_args = match param_count {
8352                                1 => vec![value.clone()],
8353                                _ => vec![value.clone(), JValue::string(key.clone())],
8354                            };
8355
8356                            let fn_result = self.apply_function(func_arg, &call_args, data)?;
8357                            // Skip undefined results (similar to map behavior)
8358                            if !fn_result.is_null() && !fn_result.is_undefined() {
8359                                result.push(fn_result);
8360                            }
8361                        }
8362                        Ok(JValue::array(result))
8363                    }
8364                    JValue::Null => Ok(JValue::Null),
8365                    _ => Err(EvaluatorError::TypeError(
8366                        "each() first argument must be an object".to_string(),
8367                    )),
8368                }
8369            }
8370
8371            "not" => {
8372                if evaluated_args.len() != 1 {
8373                    return Err(EvaluatorError::EvaluationError(
8374                        "not() requires exactly 1 argument".to_string(),
8375                    ));
8376                }
8377                // $not(x) returns the logical negation of x
8378                // null is falsy, so $not(null) = true; undefined stays undefined
8379                if evaluated_args[0].is_undefined() {
8380                    return Ok(JValue::Undefined);
8381                }
8382                Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
8383            }
8384            "boolean" => {
8385                if evaluated_args.len() != 1 {
8386                    return Err(EvaluatorError::EvaluationError(
8387                        "boolean() requires exactly 1 argument".to_string(),
8388                    ));
8389                }
8390                if evaluated_args[0].is_undefined() {
8391                    return Ok(JValue::Undefined);
8392                }
8393                Ok(functions::boolean::boolean(&evaluated_args[0])?)
8394            }
8395            "type" => {
8396                if evaluated_args.len() != 1 {
8397                    return Err(EvaluatorError::EvaluationError(
8398                        "type() requires exactly 1 argument".to_string(),
8399                    ));
8400                }
8401                // Return type string
8402                // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
8403                // We use a special marker object to distinguish undefined from null
8404                match &evaluated_args[0] {
8405                    JValue::Null => Ok(JValue::string("null")),
8406                    JValue::Bool(_) => Ok(JValue::string("boolean")),
8407                    JValue::Number(_) => Ok(JValue::string("number")),
8408                    JValue::String(_) => Ok(JValue::string("string")),
8409                    JValue::Array(_) => Ok(JValue::string("array")),
8410                    JValue::Object(_) => Ok(JValue::string("object")),
8411                    JValue::Undefined => Ok(JValue::Undefined),
8412                    JValue::Lambda { .. } | JValue::Builtin { .. } => {
8413                        Ok(JValue::string("function"))
8414                    }
8415                    JValue::Regex { .. } => Ok(JValue::string("regex")),
8416                }
8417            }
8418
8419            "base64encode" => {
8420                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8421                    return Ok(JValue::Null);
8422                }
8423                if evaluated_args.len() != 1 {
8424                    return Err(EvaluatorError::EvaluationError(
8425                        "base64encode() requires exactly 1 argument".to_string(),
8426                    ));
8427                }
8428                match &evaluated_args[0] {
8429                    JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
8430                    _ => Err(EvaluatorError::TypeError(
8431                        "base64encode() requires a string argument".to_string(),
8432                    )),
8433                }
8434            }
8435            "base64decode" => {
8436                if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8437                    return Ok(JValue::Null);
8438                }
8439                if evaluated_args.len() != 1 {
8440                    return Err(EvaluatorError::EvaluationError(
8441                        "base64decode() requires exactly 1 argument".to_string(),
8442                    ));
8443                }
8444                match &evaluated_args[0] {
8445                    JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
8446                    _ => Err(EvaluatorError::TypeError(
8447                        "base64decode() requires a string argument".to_string(),
8448                    )),
8449                }
8450            }
8451            "encodeUrlComponent" => {
8452                if evaluated_args.len() != 1 {
8453                    return Err(EvaluatorError::EvaluationError(
8454                        "encodeUrlComponent() requires exactly 1 argument".to_string(),
8455                    ));
8456                }
8457                if evaluated_args[0].is_null() {
8458                    return Ok(JValue::Null);
8459                }
8460                if evaluated_args[0].is_undefined() {
8461                    return Ok(JValue::Undefined);
8462                }
8463                match &evaluated_args[0] {
8464                    JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
8465                    _ => Err(EvaluatorError::TypeError(
8466                        "encodeUrlComponent() requires a string argument".to_string(),
8467                    )),
8468                }
8469            }
8470            "decodeUrlComponent" => {
8471                if evaluated_args.len() != 1 {
8472                    return Err(EvaluatorError::EvaluationError(
8473                        "decodeUrlComponent() requires exactly 1 argument".to_string(),
8474                    ));
8475                }
8476                if evaluated_args[0].is_null() {
8477                    return Ok(JValue::Null);
8478                }
8479                if evaluated_args[0].is_undefined() {
8480                    return Ok(JValue::Undefined);
8481                }
8482                match &evaluated_args[0] {
8483                    JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
8484                    _ => Err(EvaluatorError::TypeError(
8485                        "decodeUrlComponent() requires a string argument".to_string(),
8486                    )),
8487                }
8488            }
8489            "encodeUrl" => {
8490                if evaluated_args.len() != 1 {
8491                    return Err(EvaluatorError::EvaluationError(
8492                        "encodeUrl() requires exactly 1 argument".to_string(),
8493                    ));
8494                }
8495                if evaluated_args[0].is_null() {
8496                    return Ok(JValue::Null);
8497                }
8498                if evaluated_args[0].is_undefined() {
8499                    return Ok(JValue::Undefined);
8500                }
8501                match &evaluated_args[0] {
8502                    JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
8503                    _ => Err(EvaluatorError::TypeError(
8504                        "encodeUrl() requires a string argument".to_string(),
8505                    )),
8506                }
8507            }
8508            "decodeUrl" => {
8509                if evaluated_args.len() != 1 {
8510                    return Err(EvaluatorError::EvaluationError(
8511                        "decodeUrl() requires exactly 1 argument".to_string(),
8512                    ));
8513                }
8514                if evaluated_args[0].is_null() {
8515                    return Ok(JValue::Null);
8516                }
8517                if evaluated_args[0].is_undefined() {
8518                    return Ok(JValue::Undefined);
8519                }
8520                match &evaluated_args[0] {
8521                    JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
8522                    _ => Err(EvaluatorError::TypeError(
8523                        "decodeUrl() requires a string argument".to_string(),
8524                    )),
8525                }
8526            }
8527
8528            "error" => {
8529                // $error(message) - throw error with custom message
8530                if evaluated_args.is_empty() {
8531                    // No message provided
8532                    return Err(EvaluatorError::EvaluationError(
8533                        "D3137: $error() function evaluated".to_string(),
8534                    ));
8535                }
8536
8537                match &evaluated_args[0] {
8538                    JValue::String(s) => {
8539                        Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
8540                    }
8541                    _ => Err(EvaluatorError::TypeError(
8542                        "T0410: Argument 1 of function error does not match function signature"
8543                            .to_string(),
8544                    )),
8545                }
8546            }
8547            "assert" => {
8548                // $assert(condition, message) - throw error if condition is false
8549                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8550                    return Err(EvaluatorError::EvaluationError(
8551                        "assert() requires 1 or 2 arguments".to_string(),
8552                    ));
8553                }
8554
8555                // First argument must be a boolean
8556                let condition = match &evaluated_args[0] {
8557                    JValue::Bool(b) => *b,
8558                    _ => {
8559                        return Err(EvaluatorError::TypeError(
8560                            "T0410: Argument 1 of function $assert does not match function signature".to_string(),
8561                        ));
8562                    }
8563                };
8564
8565                if !condition {
8566                    let message = if evaluated_args.len() == 2 {
8567                        match &evaluated_args[1] {
8568                            JValue::String(s) => s.clone(),
8569                            _ => Rc::from("$assert() statement failed"),
8570                        }
8571                    } else {
8572                        Rc::from("$assert() statement failed")
8573                    };
8574                    return Err(EvaluatorError::EvaluationError(format!(
8575                        "D3141: {}",
8576                        message
8577                    )));
8578                }
8579
8580                Ok(JValue::Null)
8581            }
8582
8583            "eval" => {
8584                // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
8585                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8586                    return Err(EvaluatorError::EvaluationError(
8587                        "T0410: Argument 1 of function $eval must be a string".to_string(),
8588                    ));
8589                }
8590
8591                // If the first argument is null/undefined, return undefined
8592                if evaluated_args[0].is_null() {
8593                    return Ok(JValue::Null);
8594                }
8595                if evaluated_args[0].is_undefined() {
8596                    return Ok(JValue::Undefined);
8597                }
8598
8599                // First argument must be a string expression
8600                let expr_str = match &evaluated_args[0] {
8601                    JValue::String(s) => &**s,
8602                    _ => {
8603                        return Err(EvaluatorError::EvaluationError(
8604                            "T0410: Argument 1 of function $eval must be a string".to_string(),
8605                        ));
8606                    }
8607                };
8608
8609                // Parse the expression
8610                let parsed_ast = match parser::parse(expr_str) {
8611                    Ok(ast) => ast,
8612                    Err(e) => {
8613                        // D3120 is the error code for parse errors in $eval
8614                        return Err(EvaluatorError::EvaluationError(format!(
8615                            "D3120: The expression passed to $eval cannot be parsed: {}",
8616                            e
8617                        )));
8618                    }
8619                };
8620
8621                // Determine the context to use for evaluation
8622                let eval_context = if evaluated_args.len() == 2 {
8623                    &evaluated_args[1]
8624                } else {
8625                    data
8626                };
8627
8628                // Evaluate the parsed expression
8629                match self.evaluate_internal(&parsed_ast, eval_context) {
8630                    Ok(result) => Ok(result),
8631                    Err(e) => {
8632                        // D3121 is the error code for evaluation errors in $eval
8633                        let err_msg = e.to_string();
8634                        if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
8635                            Err(EvaluatorError::EvaluationError(format!(
8636                                "D3121: {}",
8637                                err_msg
8638                            )))
8639                        } else {
8640                            Err(e)
8641                        }
8642                    }
8643                }
8644            }
8645
8646            "now" => {
8647                if !evaluated_args.is_empty() {
8648                    return Err(EvaluatorError::EvaluationError(
8649                        "now() takes no arguments".to_string(),
8650                    ));
8651                }
8652                Ok(crate::datetime::now())
8653            }
8654
8655            "millis" => {
8656                if !evaluated_args.is_empty() {
8657                    return Err(EvaluatorError::EvaluationError(
8658                        "millis() takes no arguments".to_string(),
8659                    ));
8660                }
8661                Ok(crate::datetime::millis())
8662            }
8663
8664            "toMillis" => {
8665                if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8666                    return Err(EvaluatorError::EvaluationError(
8667                        "toMillis() requires 1 or 2 arguments".to_string(),
8668                    ));
8669                }
8670
8671                match &evaluated_args[0] {
8672                    JValue::String(s) => {
8673                        // Optional second argument is a picture string for custom parsing
8674                        if evaluated_args.len() == 2 {
8675                            match &evaluated_args[1] {
8676                                JValue::String(picture) => {
8677                                    // Use custom picture format parsing
8678                                    Ok(crate::datetime::to_millis_with_picture(s, picture)?)
8679                                }
8680                                JValue::Null => Ok(JValue::Null),
8681                                JValue::Undefined => Ok(JValue::Undefined),
8682                                _ => Err(EvaluatorError::TypeError(
8683                                    "toMillis() second argument must be a string".to_string(),
8684                                )),
8685                            }
8686                        } else {
8687                            // Use ISO 8601 partial date parsing
8688                            Ok(crate::datetime::to_millis(s)?)
8689                        }
8690                    }
8691                    JValue::Null => Ok(JValue::Null),
8692                    JValue::Undefined => Ok(JValue::Undefined),
8693                    _ => Err(EvaluatorError::TypeError(
8694                        "toMillis() requires a string argument".to_string(),
8695                    )),
8696                }
8697            }
8698
8699            "fromMillis" => {
8700                if evaluated_args.is_empty() || evaluated_args.len() > 3 {
8701                    return Err(EvaluatorError::EvaluationError(
8702                        "fromMillis() requires 1 to 3 arguments".to_string(),
8703                    ));
8704                }
8705
8706                match &evaluated_args[0] {
8707                    JValue::Number(n) => {
8708                        let millis = (if n.fract() == 0.0 {
8709                            Ok(*n as i64)
8710                        } else {
8711                            Err(())
8712                        })
8713                        .map_err(|_| {
8714                            EvaluatorError::TypeError(
8715                                "fromMillis() requires an integer".to_string(),
8716                            )
8717                        })?;
8718
8719                        let picture = match evaluated_args.get(1) {
8720                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
8721                            Some(JValue::String(s)) => Some(s.to_string()),
8722                            Some(_) => {
8723                                return Err(EvaluatorError::TypeError(
8724                                    "fromMillis() second argument must be a string".to_string(),
8725                                ))
8726                            }
8727                        };
8728                        let timezone = match evaluated_args.get(2) {
8729                            None | Some(JValue::Undefined) | Some(JValue::Null) => None,
8730                            Some(JValue::String(s)) => Some(s.to_string()),
8731                            Some(_) => {
8732                                return Err(EvaluatorError::TypeError(
8733                                    "fromMillis() third argument must be a string".to_string(),
8734                                ))
8735                            }
8736                        };
8737
8738                        Ok(crate::datetime::from_millis_with_picture(
8739                            millis,
8740                            picture.as_deref(),
8741                            timezone.as_deref(),
8742                        )?)
8743                    }
8744                    JValue::Null => Ok(JValue::Null),
8745                    JValue::Undefined => Ok(JValue::Undefined),
8746                    _ => Err(EvaluatorError::TypeError(
8747                        "fromMillis() requires a number argument".to_string(),
8748                    )),
8749                }
8750            }
8751
8752            _ => Err(EvaluatorError::ReferenceError(format!(
8753                "Unknown function: {}",
8754                name
8755            ))),
8756        }
8757    }
8758
8759    /// Apply a function (lambda or expression) to values
8760    ///
8761    /// This handles both:
8762    /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
8763    /// 2. Simple expressions: price * 2 - evaluates with values as context
8764    fn apply_function(
8765        &mut self,
8766        func_node: &AstNode,
8767        values: &[JValue],
8768        data: &JValue,
8769    ) -> Result<JValue, EvaluatorError> {
8770        match func_node {
8771            AstNode::Lambda {
8772                params,
8773                body,
8774                signature,
8775                thunk,
8776            } => {
8777                // Direct lambda - invoke it
8778                self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
8779            }
8780            AstNode::Function {
8781                name,
8782                args,
8783                is_builtin,
8784            } => {
8785                // Function call - check if it has placeholders (partial application)
8786                let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
8787
8788                if has_placeholder {
8789                    // This is a partial application - evaluate it to get the lambda value
8790                    let partial_lambda =
8791                        self.create_partial_application(name, args, *is_builtin, data)?;
8792
8793                    // Now invoke the partial lambda with the provided values
8794                    if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
8795                        return self.invoke_stored_lambda(&stored, values, data);
8796                    }
8797                    Err(EvaluatorError::EvaluationError(
8798                        "Failed to apply partial application".to_string(),
8799                    ))
8800                } else {
8801                    // Regular function call without placeholders
8802                    // Evaluate it and apply if it returns a function
8803                    let result = self.evaluate_internal(func_node, data)?;
8804
8805                    // Check if result is a lambda value
8806                    if let Some(stored) = self.lookup_lambda_from_value(&result) {
8807                        return self.invoke_stored_lambda(&stored, values, data);
8808                    }
8809
8810                    // Otherwise just return the result
8811                    Ok(result)
8812                }
8813            }
8814            AstNode::Variable(var_name) => {
8815                // Check if this variable holds a stored lambda
8816                if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
8817                    self.invoke_stored_lambda(&stored_lambda, values, data)
8818                } else if let Some(value) = self.context.lookup(var_name).cloned() {
8819                    // Check if this variable holds a lambda value
8820                    // This handles lambdas passed as bound arguments in partial applications
8821                    if let Some(stored) = self.lookup_lambda_from_value(&value) {
8822                        return self.invoke_stored_lambda(&stored, values, data);
8823                    }
8824                    // Regular variable value - evaluate with first value as context
8825                    if values.is_empty() {
8826                        self.evaluate_internal(func_node, data)
8827                    } else {
8828                        self.evaluate_internal(func_node, &values[0])
8829                    }
8830                } else if self.is_builtin_function(var_name) {
8831                    // This is a built-in function reference (e.g., $string, $number)
8832                    // Call it directly with the provided values (already evaluated)
8833                    self.call_builtin_with_values(var_name, values)
8834                } else {
8835                    // Unknown variable - evaluate with first value as context
8836                    if values.is_empty() {
8837                        self.evaluate_internal(func_node, data)
8838                    } else {
8839                        self.evaluate_internal(func_node, &values[0])
8840                    }
8841                }
8842            }
8843            _ => {
8844                // For non-lambda expressions, evaluate with first value as context
8845                if values.is_empty() {
8846                    self.evaluate_internal(func_node, data)
8847                } else {
8848                    self.evaluate_internal(func_node, &values[0])
8849                }
8850            }
8851        }
8852    }
8853
8854    /// Execute a transform operator on the bound $ value
8855    fn execute_transform(
8856        &mut self,
8857        location: &AstNode,
8858        update: &AstNode,
8859        delete: Option<&AstNode>,
8860        _original_data: &JValue,
8861    ) -> Result<JValue, EvaluatorError> {
8862        // Get the input value from $ binding
8863        let input = self
8864            .context
8865            .lookup("$")
8866            .ok_or_else(|| {
8867                EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
8868            })?
8869            .clone();
8870
8871        // Evaluate location expression on the input to get objects to transform
8872        let located_objects = self.evaluate_internal(location, &input)?;
8873
8874        // Collect target objects into a vector for comparison
8875        let targets: Vec<JValue> = match located_objects {
8876            JValue::Array(arr) => arr.to_vec(),
8877            JValue::Object(_) => vec![located_objects],
8878            JValue::Null => Vec::new(),
8879            other => vec![other],
8880        };
8881
8882        // Validate update parameter - must be an object constructor
8883        // We need to check this before evaluation in case of errors
8884        // For now, we'll validate after evaluation in the transform helper
8885
8886        // Parse delete field names if provided
8887        let delete_fields: Vec<String> = if let Some(delete_node) = delete {
8888            let delete_val = self.evaluate_internal(delete_node, &input)?;
8889            match delete_val {
8890                JValue::Array(arr) => arr
8891                    .iter()
8892                    .filter_map(|v| match v {
8893                        JValue::String(s) => Some(s.to_string()),
8894                        _ => None,
8895                    })
8896                    .collect(),
8897                JValue::String(s) => vec![s.to_string()],
8898                JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
8899                _ => {
8900                    // Delete parameter must be an array of strings or a string
8901                    return Err(EvaluatorError::EvaluationError(
8902                        "T2012: The third argument of the transform operator must be an array of strings".to_string()
8903                    ));
8904                }
8905            }
8906        } else {
8907            Vec::new()
8908        };
8909
8910        // Recursive helper to apply transformation throughout the structure
8911        fn apply_transform_deep(
8912            evaluator: &mut Evaluator,
8913            value: &JValue,
8914            targets: &[JValue],
8915            update: &AstNode,
8916            delete_fields: &[String],
8917        ) -> Result<JValue, EvaluatorError> {
8918            // Check if this value is one of the targets to transform
8919            // Use JValue's PartialEq for semantic equality comparison
8920            if targets.iter().any(|t| t == value) {
8921                // Transform this object
8922                if let JValue::Object(map_rc) = value.clone() {
8923                    let mut map = (*map_rc).clone();
8924                    let update_val = evaluator.evaluate_internal(update, value)?;
8925                    // Validate that update evaluates to an object or null (undefined)
8926                    match update_val {
8927                        JValue::Object(update_map) => {
8928                            for (key, val) in update_map.iter() {
8929                                map.insert(key.clone(), val.clone());
8930                            }
8931                        }
8932                        JValue::Null | JValue::Undefined => {
8933                            // Null/undefined means no updates, just continue to deletions
8934                        }
8935                        _ => {
8936                            return Err(EvaluatorError::EvaluationError(
8937                                "T2011: The second argument of the transform operator must evaluate to an object".to_string()
8938                            ));
8939                        }
8940                    }
8941                    for field in delete_fields {
8942                        map.shift_remove(field);
8943                    }
8944                    return Ok(JValue::object(map));
8945                }
8946                return Ok(value.clone());
8947            }
8948
8949            // Otherwise, recursively process children to find and transform targets
8950            match value {
8951                JValue::Object(map) => {
8952                    let mut new_map = IndexMap::new();
8953                    for (k, v) in map.iter() {
8954                        new_map.insert(
8955                            k.clone(),
8956                            apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
8957                        );
8958                    }
8959                    Ok(JValue::object(new_map))
8960                }
8961                JValue::Array(arr) => {
8962                    let mut new_arr = Vec::new();
8963                    for item in arr.iter() {
8964                        new_arr.push(apply_transform_deep(
8965                            evaluator,
8966                            item,
8967                            targets,
8968                            update,
8969                            delete_fields,
8970                        )?);
8971                    }
8972                    Ok(JValue::array(new_arr))
8973                }
8974                _ => Ok(value.clone()),
8975            }
8976        }
8977
8978        // Apply transformation recursively starting from input
8979        apply_transform_deep(self, &input, &targets, update, &delete_fields)
8980    }
8981
8982    /// Helper to invoke a lambda with given parameters
8983    fn invoke_lambda(
8984        &mut self,
8985        params: &[String],
8986        body: &AstNode,
8987        signature: Option<&String>,
8988        values: &[JValue],
8989        data: &JValue,
8990        thunk: bool,
8991    ) -> Result<JValue, EvaluatorError> {
8992        self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
8993    }
8994
8995    /// Invoke a lambda with optional captured environment (for closures)
8996    fn invoke_lambda_with_env(
8997        &mut self,
8998        params: &[String],
8999        body: &AstNode,
9000        signature: Option<&String>,
9001        values: &[JValue],
9002        data: &JValue,
9003        captured_env: Option<&HashMap<String, JValue>>,
9004        captured_data: Option<&JValue>,
9005        thunk: bool,
9006    ) -> Result<JValue, EvaluatorError> {
9007        // If this is a thunk (has tail calls), use TCO trampoline
9008        if thunk {
9009            let stored = StoredLambda {
9010                params: params.to_vec(),
9011                body: body.clone(),
9012                compiled_body: None, // Thunks use TCO, not the compiled fast path
9013                signature: signature.cloned(),
9014                captured_env: captured_env.cloned().unwrap_or_default(),
9015                captured_data: captured_data.cloned(),
9016                thunk,
9017            };
9018            return self.invoke_lambda_with_tco(&stored, values, data);
9019        }
9020
9021        // Validate signature if present, and get coerced arguments
9022        // Push a new scope for this lambda invocation
9023        self.context.push_scope();
9024
9025        // First apply captured environment (for closures)
9026        if let Some(env) = captured_env {
9027            for (name, value) in env {
9028                self.context.bind(name.clone(), value.clone());
9029            }
9030        }
9031
9032        if let Some(sig_str) = signature {
9033            // Validate and coerce arguments with signature
9034            let coerced_values = match crate::signature::Signature::parse(sig_str) {
9035                Ok(sig) => match sig.validate_and_coerce(values, data) {
9036                    Ok(coerced) => coerced,
9037                    Err(e) => {
9038                        self.context.pop_scope();
9039                        match e {
9040                            crate::signature::SignatureError::UndefinedArgument => {
9041                                return Ok(JValue::Null);
9042                            }
9043                            crate::signature::SignatureError::ArgumentTypeMismatch {
9044                                index,
9045                                expected,
9046                            } => {
9047                                return Err(EvaluatorError::TypeError(
9048                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9049                                    ));
9050                            }
9051                            crate::signature::SignatureError::ArrayTypeMismatch {
9052                                index,
9053                                expected,
9054                            } => {
9055                                return Err(EvaluatorError::TypeError(format!(
9056                                    "T0412: Argument {} of function must be an array of {}",
9057                                    index, expected
9058                                )));
9059                            }
9060                            crate::signature::SignatureError::ContextTypeMismatch {
9061                                index,
9062                                expected,
9063                            } => {
9064                                return Err(EvaluatorError::TypeError(format!(
9065                                    "T0411: Context value at argument {} does not match function signature (expected {})",
9066                                    index, expected
9067                                )));
9068                            }
9069                            _ => {
9070                                return Err(EvaluatorError::TypeError(format!(
9071                                    "Signature validation failed: {}",
9072                                    e
9073                                )));
9074                            }
9075                        }
9076                    }
9077                },
9078                Err(e) => {
9079                    self.context.pop_scope();
9080                    return Err(EvaluatorError::EvaluationError(format!(
9081                        "Invalid signature: {}",
9082                        e
9083                    )));
9084                }
9085            };
9086            // Bind coerced values to params
9087            for (i, param) in params.iter().enumerate() {
9088                let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9089                self.context.bind(param.clone(), value);
9090            }
9091        } else {
9092            // No signature - bind directly from values slice (no allocation)
9093            for (i, param) in params.iter().enumerate() {
9094                let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9095                self.context.bind(param.clone(), value);
9096            }
9097        }
9098
9099        // Check if this is a partial application (body is a special marker string)
9100        if let AstNode::String(body_str) = body {
9101            if body_str.starts_with("__partial_call:") {
9102                // Parse the partial call info
9103                let parts: Vec<&str> = body_str.split(':').collect();
9104                if parts.len() >= 4 {
9105                    let func_name = parts[1];
9106                    let is_builtin = parts[2] == "true";
9107                    let total_args: usize = parts[3].parse().unwrap_or(0);
9108
9109                    // Get placeholder positions from captured env
9110                    let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
9111                        if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
9112                            positions
9113                                .iter()
9114                                .filter_map(|v| v.as_f64().map(|n| n as usize))
9115                                .collect()
9116                        } else {
9117                            vec![]
9118                        }
9119                    } else {
9120                        vec![]
9121                    };
9122
9123                    // Reconstruct the full argument list
9124                    let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
9125
9126                    // Fill in bound arguments from captured environment
9127                    if let Some(env) = captured_env {
9128                        for (key, value) in env {
9129                            if key.starts_with("__bound_arg_") {
9130                                if let Ok(pos) = key[12..].parse::<usize>() {
9131                                    if pos < total_args {
9132                                        full_args[pos] = value.clone();
9133                                    }
9134                                }
9135                            }
9136                        }
9137                    }
9138
9139                    // Fill in placeholder positions with provided values
9140                    for (i, &pos) in placeholder_positions.iter().enumerate() {
9141                        if pos < total_args {
9142                            let value = values.get(i).cloned().unwrap_or(JValue::Null);
9143                            full_args[pos] = value;
9144                        }
9145                    }
9146
9147                    // Pop lambda scope, then push a new scope for temp args
9148                    self.context.pop_scope();
9149                    self.context.push_scope();
9150
9151                    // Build AST nodes for the function call arguments
9152                    let mut temp_args: Vec<AstNode> = Vec::new();
9153                    for (i, value) in full_args.iter().enumerate() {
9154                        let temp_name = format!("__temp_arg_{}", i);
9155                        self.context.bind(temp_name.clone(), value.clone());
9156                        temp_args.push(AstNode::Variable(temp_name));
9157                    }
9158
9159                    // Call the original function
9160                    let result =
9161                        self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
9162
9163                    // Pop temp scope
9164                    self.context.pop_scope();
9165
9166                    return result;
9167                }
9168            }
9169        }
9170
9171        // Evaluate lambda body (normal case)
9172        // Use captured_data for lexical scoping if available, otherwise use call-site data
9173        let body_data = captured_data.unwrap_or(data);
9174        let result = self.evaluate_internal(body, body_data)?;
9175
9176        // Pop lambda scope, preserving any lambdas referenced by the return value
9177        // Fast path: scalar results can never contain lambda references
9178        let is_scalar = matches!(
9179            &result,
9180            JValue::Number(_)
9181                | JValue::Bool(_)
9182                | JValue::String(_)
9183                | JValue::Null
9184                | JValue::Undefined
9185        );
9186        if is_scalar {
9187            self.context.pop_scope();
9188        } else {
9189            let lambdas_to_keep = self.extract_lambda_ids(&result);
9190            self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9191        }
9192
9193        Ok(result)
9194    }
9195
9196    /// Invoke a lambda with tail call optimization using a trampoline
9197    /// This method uses an iterative loop to handle tail-recursive calls without
9198    /// growing the stack, enabling deep recursion for tail-recursive functions.
9199    fn invoke_lambda_with_tco(
9200        &mut self,
9201        stored_lambda: &StoredLambda,
9202        initial_args: &[JValue],
9203        data: &JValue,
9204    ) -> Result<JValue, EvaluatorError> {
9205        let mut current_lambda = stored_lambda.clone();
9206        let mut current_args = initial_args.to_vec();
9207        let mut current_data = data.clone();
9208
9209        // Maximum number of tail call iterations to prevent infinite loops
9210        // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
9211        const MAX_TCO_ITERATIONS: usize = 100_000;
9212        let mut iterations = 0;
9213
9214        // Push a persistent scope for the TCO trampoline loop.
9215        // This scope persists across all iterations so that lambdas defined
9216        // in one iteration (like recursive $iter) remain available in subsequent ones.
9217        self.context.push_scope();
9218
9219        // Trampoline loop - keeps evaluating until we get a final value
9220        let result = loop {
9221            iterations += 1;
9222            if iterations > MAX_TCO_ITERATIONS {
9223                self.context.pop_scope();
9224                return Err(EvaluatorError::EvaluationError(
9225                    "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
9226                ));
9227            }
9228
9229            // Evaluate the lambda body within the persistent scope
9230            let result =
9231                self.invoke_lambda_body_for_tco(&current_lambda, &current_args, &current_data)?;
9232
9233            match result {
9234                LambdaResult::JValue(v) => break v,
9235                LambdaResult::TailCall { lambda, args, data } => {
9236                    // Continue with the tail call - no stack growth
9237                    current_lambda = *lambda;
9238                    current_args = args;
9239                    current_data = data;
9240                }
9241            }
9242        };
9243
9244        // Pop the persistent TCO scope, preserving lambdas referenced by the result
9245        let lambdas_to_keep = self.extract_lambda_ids(&result);
9246        self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9247
9248        Ok(result)
9249    }
9250
9251    /// Evaluate a lambda body, detecting tail calls for TCO
9252    /// Returns either a final value or a tail call continuation.
9253    /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
9254    /// manages the persistent scope for the trampoline loop.
9255    fn invoke_lambda_body_for_tco(
9256        &mut self,
9257        lambda: &StoredLambda,
9258        values: &[JValue],
9259        data: &JValue,
9260    ) -> Result<LambdaResult, EvaluatorError> {
9261        // Validate signature if present
9262        let coerced_values = if let Some(sig_str) = &lambda.signature {
9263            match crate::signature::Signature::parse(sig_str) {
9264                Ok(sig) => match sig.validate_and_coerce(values, data) {
9265                    Ok(coerced) => coerced,
9266                    Err(e) => match e {
9267                        crate::signature::SignatureError::UndefinedArgument => {
9268                            return Ok(LambdaResult::JValue(JValue::Null));
9269                        }
9270                        crate::signature::SignatureError::ArgumentTypeMismatch {
9271                            index,
9272                            expected,
9273                        } => {
9274                            return Err(EvaluatorError::TypeError(
9275                                        format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9276                                    ));
9277                        }
9278                        crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
9279                            return Err(EvaluatorError::TypeError(format!(
9280                                "T0412: Argument {} of function must be an array of {}",
9281                                index, expected
9282                            )));
9283                        }
9284                        crate::signature::SignatureError::ContextTypeMismatch {
9285                            index,
9286                            expected,
9287                        } => {
9288                            return Err(EvaluatorError::TypeError(format!(
9289                                "T0411: Context value at argument {} does not match function signature (expected {})",
9290                                index, expected
9291                            )));
9292                        }
9293                        _ => {
9294                            return Err(EvaluatorError::TypeError(format!(
9295                                "Signature validation failed: {}",
9296                                e
9297                            )));
9298                        }
9299                    },
9300                },
9301                Err(e) => {
9302                    return Err(EvaluatorError::EvaluationError(format!(
9303                        "Invalid signature: {}",
9304                        e
9305                    )));
9306                }
9307            }
9308        } else {
9309            values.to_vec()
9310        };
9311
9312        // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
9313        // Apply captured environment
9314        for (name, value) in &lambda.captured_env {
9315            self.context.bind(name.clone(), value.clone());
9316        }
9317
9318        // Bind parameters
9319        for (i, param) in lambda.params.iter().enumerate() {
9320            let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
9321            self.context.bind(param.clone(), value);
9322        }
9323
9324        // Evaluate the body with tail call detection
9325        let body_data = lambda.captured_data.as_ref().unwrap_or(data);
9326        self.evaluate_for_tco(&lambda.body, body_data)
9327    }
9328
9329    /// Evaluate an expression for TCO, detecting tail calls
9330    /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
9331    fn evaluate_for_tco(
9332        &mut self,
9333        node: &AstNode,
9334        data: &JValue,
9335    ) -> Result<LambdaResult, EvaluatorError> {
9336        match node {
9337            // Conditional: evaluate condition, then evaluate the chosen branch for TCO
9338            AstNode::Conditional {
9339                condition,
9340                then_branch,
9341                else_branch,
9342            } => {
9343                let cond_value = self.evaluate_internal(condition, data)?;
9344                let is_truthy = self.is_truthy(&cond_value);
9345
9346                if is_truthy {
9347                    self.evaluate_for_tco(then_branch, data)
9348                } else if let Some(else_expr) = else_branch {
9349                    self.evaluate_for_tco(else_expr, data)
9350                } else {
9351                    Ok(LambdaResult::JValue(JValue::Null))
9352                }
9353            }
9354
9355            // Block: evaluate all but last normally, last for TCO
9356            AstNode::Block(exprs) => {
9357                if exprs.is_empty() {
9358                    return Ok(LambdaResult::JValue(JValue::Null));
9359                }
9360
9361                // Evaluate all expressions except the last
9362                let mut result = JValue::Null;
9363                for (i, expr) in exprs.iter().enumerate() {
9364                    if i == exprs.len() - 1 {
9365                        // Last expression - evaluate for TCO
9366                        return self.evaluate_for_tco(expr, data);
9367                    } else {
9368                        result = self.evaluate_internal(expr, data)?;
9369                    }
9370                }
9371                Ok(LambdaResult::JValue(result))
9372            }
9373
9374            // Variable binding: evaluate value, bind, then evaluate result for TCO if present
9375            AstNode::Binary {
9376                op: BinaryOp::ColonEqual,
9377                lhs,
9378                rhs,
9379            } => {
9380                // This is var := value; get the variable name
9381                let var_name = match lhs.as_ref() {
9382                    AstNode::Variable(name) => name.clone(),
9383                    _ => {
9384                        // Not a simple variable binding, evaluate normally
9385                        let result = self.evaluate_internal(node, data)?;
9386                        return Ok(LambdaResult::JValue(result));
9387                    }
9388                };
9389
9390                // Check if RHS is a lambda - store it specially
9391                if let AstNode::Lambda {
9392                    params,
9393                    body,
9394                    signature,
9395                    thunk,
9396                } = rhs.as_ref()
9397                {
9398                    let captured_env = self.capture_environment_for(body, params);
9399                    let compiled_body = if !thunk {
9400                        let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9401                        try_compile_expr_with_allowed_vars(body, &var_refs)
9402                    } else {
9403                        None
9404                    };
9405                    let stored_lambda = StoredLambda {
9406                        params: params.clone(),
9407                        body: (**body).clone(),
9408                        compiled_body,
9409                        signature: signature.clone(),
9410                        captured_env,
9411                        captured_data: Some(data.clone()),
9412                        thunk: *thunk,
9413                    };
9414                    self.context.bind_lambda(var_name, stored_lambda);
9415                    let lambda_repr =
9416                        JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
9417                    return Ok(LambdaResult::JValue(lambda_repr));
9418                }
9419
9420                // Evaluate the RHS
9421                let value = self.evaluate_internal(rhs, data)?;
9422                self.context.bind(var_name, value.clone());
9423                Ok(LambdaResult::JValue(value))
9424            }
9425
9426            // Function call - this is where TCO happens
9427            AstNode::Function { name, args, .. } => {
9428                // Check if this is a call to a stored lambda (user function)
9429                if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
9430                    if stored_lambda.thunk {
9431                        let mut evaluated_args = Vec::with_capacity(args.len());
9432                        for arg in args {
9433                            evaluated_args.push(self.evaluate_internal(arg, data)?);
9434                        }
9435                        return Ok(LambdaResult::TailCall {
9436                            lambda: Box::new(stored_lambda),
9437                            args: evaluated_args,
9438                            data: data.clone(),
9439                        });
9440                    }
9441                }
9442                // Not a thunk lambda - evaluate normally
9443                let result = self.evaluate_internal(node, data)?;
9444                Ok(LambdaResult::JValue(result))
9445            }
9446
9447            // Call node (calling a lambda value)
9448            AstNode::Call { procedure, args } => {
9449                // Evaluate the procedure to get the callable
9450                let callable = self.evaluate_internal(procedure, data)?;
9451
9452                // Check if it's a lambda with TCO
9453                if let JValue::Lambda { lambda_id, .. } = &callable {
9454                    if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
9455                        if stored_lambda.thunk {
9456                            let mut evaluated_args = Vec::with_capacity(args.len());
9457                            for arg in args {
9458                                evaluated_args.push(self.evaluate_internal(arg, data)?);
9459                            }
9460                            return Ok(LambdaResult::TailCall {
9461                                lambda: Box::new(stored_lambda),
9462                                args: evaluated_args,
9463                                data: data.clone(),
9464                            });
9465                        }
9466                    }
9467                }
9468                // Not a thunk - evaluate normally
9469                let result = self.evaluate_internal(node, data)?;
9470                Ok(LambdaResult::JValue(result))
9471            }
9472
9473            // Variable reference that might be a function call
9474            // This handles cases like $f($x) where $f is referenced by name
9475            AstNode::Variable(_) => {
9476                let result = self.evaluate_internal(node, data)?;
9477                Ok(LambdaResult::JValue(result))
9478            }
9479
9480            // Any other expression - evaluate normally
9481            _ => {
9482                let result = self.evaluate_internal(node, data)?;
9483                Ok(LambdaResult::JValue(result))
9484            }
9485        }
9486    }
9487
9488    /// Match with custom matcher function
9489    ///
9490    /// Implements custom matcher support for $match(str, matcherFunction, limit?)
9491    /// The matcher function is called with the string and returns:
9492    /// { match: string, start: number, end: number, groups: [], next: function }
9493    /// The next function is called repeatedly to get subsequent matches
9494    fn match_with_custom_matcher(
9495        &mut self,
9496        str_value: &str,
9497        matcher_node: &AstNode,
9498        limit: Option<usize>,
9499        data: &JValue,
9500    ) -> Result<JValue, EvaluatorError> {
9501        let mut results = Vec::new();
9502        let mut count = 0;
9503
9504        // Call the matcher function with the string
9505        let str_val = JValue::string(str_value.to_string());
9506        let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
9507
9508        // Iterate through matches following the 'next' chain
9509        while !current_match.is_undefined() && !current_match.is_null() {
9510            // Check limit
9511            if let Some(lim) = limit {
9512                if count >= lim {
9513                    break;
9514                }
9515            }
9516
9517            // Extract match information from the result object
9518            if let JValue::Object(ref match_obj) = current_match {
9519                // Validate that this is a proper match object
9520                let has_match = match_obj.contains_key("match");
9521                let has_start = match_obj.contains_key("start");
9522                let has_end = match_obj.contains_key("end");
9523                let has_groups = match_obj.contains_key("groups");
9524                let has_next = match_obj.contains_key("next");
9525
9526                if !has_match && !has_start && !has_end && !has_groups && !has_next {
9527                    // Invalid matcher result - T1010 error
9528                    return Err(EvaluatorError::EvaluationError(
9529                        "T1010: The matcher function did not return the correct object structure"
9530                            .to_string(),
9531                    ));
9532                }
9533
9534                // Build the result match object (match, index, groups)
9535                let mut result_obj = IndexMap::new();
9536
9537                if let Some(match_val) = match_obj.get("match") {
9538                    result_obj.insert("match".to_string(), match_val.clone());
9539                }
9540
9541                if let Some(start_val) = match_obj.get("start") {
9542                    result_obj.insert("index".to_string(), start_val.clone());
9543                }
9544
9545                if let Some(groups_val) = match_obj.get("groups") {
9546                    result_obj.insert("groups".to_string(), groups_val.clone());
9547                }
9548
9549                results.push(JValue::object(result_obj));
9550                count += 1;
9551
9552                // Get the next match by calling the 'next' function
9553                if let Some(next_func) = match_obj.get("next") {
9554                    if let Some(stored) = self.lookup_lambda_from_value(next_func) {
9555                        current_match = self.invoke_stored_lambda(&stored, &[], data)?;
9556                        continue;
9557                    }
9558                }
9559
9560                // No next function or couldn't call it - stop iteration
9561                break;
9562            } else {
9563                // Not a valid match object
9564                break;
9565            }
9566        }
9567
9568        // Return results
9569        if results.is_empty() {
9570            Ok(JValue::Undefined)
9571        } else {
9572            Ok(JValue::array(results))
9573        }
9574    }
9575
9576    /// Replace with lambda/function callback
9577    ///
9578    /// Implements lambda replacement for $replace(str, pattern, function, limit?)
9579    /// The function receives a match object with: match, start, end, groups
9580    fn replace_with_lambda(
9581        &mut self,
9582        str_value: &JValue,
9583        pattern_value: &JValue,
9584        lambda_value: &JValue,
9585        limit_value: Option<&JValue>,
9586        data: &JValue,
9587    ) -> Result<JValue, EvaluatorError> {
9588        // Extract string
9589        let s = match str_value {
9590            JValue::String(s) => &**s,
9591            _ => {
9592                return Err(EvaluatorError::TypeError(
9593                    "replace() requires string arguments".to_string(),
9594                ))
9595            }
9596        };
9597
9598        // Extract regex pattern
9599        let (pattern, flags) =
9600            crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
9601                EvaluatorError::TypeError(
9602                    "replace() pattern must be a regex when using lambda replacement".to_string(),
9603                )
9604            })?;
9605
9606        // Build regex
9607        let re = crate::functions::string::build_regex(&pattern, &flags)?;
9608
9609        // Parse limit
9610        let limit = if let Some(lim_val) = limit_value {
9611            match lim_val {
9612                JValue::Number(n) => {
9613                    let lim_f64 = *n;
9614                    if lim_f64 < 0.0 {
9615                        return Err(EvaluatorError::EvaluationError(format!(
9616                            "D3011: Limit must be non-negative, got {}",
9617                            lim_f64
9618                        )));
9619                    }
9620                    Some(lim_f64 as usize)
9621                }
9622                _ => {
9623                    return Err(EvaluatorError::TypeError(
9624                        "replace() limit must be a number".to_string(),
9625                    ))
9626                }
9627            }
9628        } else {
9629            None
9630        };
9631
9632        // Iterate through matches and replace using lambda
9633        let mut result = String::new();
9634        let mut last_end = 0;
9635        let mut count = 0;
9636
9637        for cap in re.captures_iter(s) {
9638            // Check limit
9639            if let Some(lim) = limit {
9640                if count >= lim {
9641                    break;
9642                }
9643            }
9644
9645            let m = cap.get(0).unwrap();
9646            let match_start = m.start();
9647            let match_end = m.end();
9648            let match_str = m.as_str();
9649
9650            // Add text before match
9651            result.push_str(&s[last_end..match_start]);
9652
9653            // Build match object
9654            let groups: Vec<JValue> = (1..cap.len())
9655                .map(|i| {
9656                    cap.get(i)
9657                        .map(|m| JValue::string(m.as_str().to_string()))
9658                        .unwrap_or(JValue::Null)
9659                })
9660                .collect();
9661
9662            let mut match_map = IndexMap::new();
9663            match_map.insert("match".to_string(), JValue::string(match_str));
9664            match_map.insert("start".to_string(), JValue::Number(match_start as f64));
9665            match_map.insert("end".to_string(), JValue::Number(match_end as f64));
9666            match_map.insert("groups".to_string(), JValue::array(groups));
9667            let match_obj = JValue::object(match_map);
9668
9669            // Invoke lambda with match object
9670            let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
9671                EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
9672            })?;
9673            let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
9674            let replacement_str = match lambda_result {
9675                JValue::String(s) => s,
9676                _ => {
9677                    return Err(EvaluatorError::TypeError(format!(
9678                        "D3012: Replacement function must return a string, got {:?}",
9679                        lambda_result
9680                    )))
9681                }
9682            };
9683
9684            // Add replacement
9685            result.push_str(&replacement_str);
9686
9687            last_end = match_end;
9688            count += 1;
9689        }
9690
9691        // Add remaining text after last match
9692        result.push_str(&s[last_end..]);
9693
9694        Ok(JValue::string(result))
9695    }
9696
9697    /// Capture the current environment bindings for closure support
9698    fn capture_current_environment(&self) -> HashMap<String, JValue> {
9699        self.context.all_bindings()
9700    }
9701
9702    /// Capture only the variables referenced by a lambda body (selective capture).
9703    /// This avoids cloning the entire environment when only a few variables are needed.
9704    fn capture_environment_for(
9705        &self,
9706        body: &AstNode,
9707        params: &[String],
9708    ) -> HashMap<String, JValue> {
9709        let free_vars = Self::collect_free_variables(body, params);
9710        if free_vars.is_empty() {
9711            return HashMap::new();
9712        }
9713        let mut result = HashMap::new();
9714        for var_name in &free_vars {
9715            if let Some(value) = self.context.lookup(var_name) {
9716                result.insert(var_name.clone(), value.clone());
9717            }
9718        }
9719        result
9720    }
9721
9722    /// Collect all free variables in an AST node that are not bound by the given params.
9723    /// A "free variable" is one that is referenced but not defined within the expression.
9724    fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
9725        let mut free_vars = HashSet::new();
9726        let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
9727        Self::collect_free_vars_walk(body, &bound, &mut free_vars);
9728        free_vars
9729    }
9730
9731    fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
9732        match node {
9733            AstNode::Variable(name) => {
9734                if !bound.contains(name.as_str()) {
9735                    free.insert(name.clone());
9736                }
9737            }
9738            AstNode::Function { name, args, .. } => {
9739                // Function name references a variable (e.g., $f(...))
9740                if !bound.contains(name.as_str()) {
9741                    free.insert(name.clone());
9742                }
9743                for arg in args {
9744                    Self::collect_free_vars_walk(arg, bound, free);
9745                }
9746            }
9747            AstNode::Lambda { params, body, .. } => {
9748                // Inner lambda introduces new bindings
9749                let mut inner_bound = bound.clone();
9750                for p in params {
9751                    inner_bound.insert(p.as_str());
9752                }
9753                Self::collect_free_vars_walk(body, &inner_bound, free);
9754            }
9755            AstNode::Binary { op, lhs, rhs } => {
9756                Self::collect_free_vars_walk(lhs, bound, free);
9757                Self::collect_free_vars_walk(rhs, bound, free);
9758                // For ColonEqual, note: the binding is visible after this expr in blocks,
9759                // but block handling takes care of that separately
9760                let _ = op;
9761            }
9762            AstNode::Unary { operand, .. } => {
9763                Self::collect_free_vars_walk(operand, bound, free);
9764            }
9765            AstNode::Path { steps } => {
9766                for step in steps {
9767                    Self::collect_free_vars_walk(&step.node, bound, free);
9768                    for stage in &step.stages {
9769                        match stage {
9770                            Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
9771                            // An index stage binds a variable; it introduces no
9772                            // free variable references.
9773                            Stage::Index(_) => {}
9774                        }
9775                    }
9776                }
9777            }
9778            AstNode::Call { procedure, args } => {
9779                Self::collect_free_vars_walk(procedure, bound, free);
9780                for arg in args {
9781                    Self::collect_free_vars_walk(arg, bound, free);
9782                }
9783            }
9784            AstNode::Conditional {
9785                condition,
9786                then_branch,
9787                else_branch,
9788            } => {
9789                Self::collect_free_vars_walk(condition, bound, free);
9790                Self::collect_free_vars_walk(then_branch, bound, free);
9791                if let Some(else_expr) = else_branch {
9792                    Self::collect_free_vars_walk(else_expr, bound, free);
9793                }
9794            }
9795            AstNode::Block(exprs) => {
9796                let mut block_bound = bound.clone();
9797                for expr in exprs {
9798                    Self::collect_free_vars_walk(expr, &block_bound, free);
9799                    // Bindings introduced via := become bound for subsequent expressions
9800                    if let AstNode::Binary {
9801                        op: BinaryOp::ColonEqual,
9802                        lhs,
9803                        ..
9804                    } = expr
9805                    {
9806                        if let AstNode::Variable(var_name) = lhs.as_ref() {
9807                            block_bound.insert(var_name.as_str());
9808                        }
9809                    }
9810                }
9811            }
9812            AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
9813                for expr in exprs {
9814                    Self::collect_free_vars_walk(expr, bound, free);
9815                }
9816            }
9817            AstNode::Object(pairs) => {
9818                for (key, value) in pairs {
9819                    Self::collect_free_vars_walk(key, bound, free);
9820                    Self::collect_free_vars_walk(value, bound, free);
9821                }
9822            }
9823            AstNode::ObjectTransform { input, pattern } => {
9824                Self::collect_free_vars_walk(input, bound, free);
9825                for (key, value) in pattern {
9826                    Self::collect_free_vars_walk(key, bound, free);
9827                    Self::collect_free_vars_walk(value, bound, free);
9828                }
9829            }
9830            AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
9831                Self::collect_free_vars_walk(expr, bound, free);
9832            }
9833            AstNode::Sort { input, terms } => {
9834                Self::collect_free_vars_walk(input, bound, free);
9835                for (expr, _) in terms {
9836                    Self::collect_free_vars_walk(expr, bound, free);
9837                }
9838            }
9839            AstNode::Transform {
9840                location,
9841                update,
9842                delete,
9843            } => {
9844                Self::collect_free_vars_walk(location, bound, free);
9845                Self::collect_free_vars_walk(update, bound, free);
9846                if let Some(del) = delete {
9847                    Self::collect_free_vars_walk(del, bound, free);
9848                }
9849            }
9850            // Leaf nodes with no variable references
9851            AstNode::String(_)
9852            | AstNode::Name(_)
9853            | AstNode::Number(_)
9854            | AstNode::Boolean(_)
9855            | AstNode::Null
9856            | AstNode::Undefined
9857            | AstNode::Placeholder
9858            | AstNode::Regex { .. }
9859            | AstNode::Wildcard
9860            | AstNode::Descendant
9861            | AstNode::Parent(_)
9862            | AstNode::ParentVariable(_) => {}
9863        }
9864    }
9865
9866    /// Check if a name refers to a built-in function
9867    fn is_builtin_function(&self, name: &str) -> bool {
9868        matches!(
9869            name,
9870            // String functions
9871            "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
9872            "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
9873            "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
9874            "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
9875
9876            // Numeric functions
9877            "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
9878            "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
9879
9880            // Aggregation functions
9881            "sum" | "max" | "min" | "average" |
9882
9883            // Boolean/logic functions
9884            "boolean" | "not" | "exists" |
9885
9886            // Array functions
9887            "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
9888
9889            // Object functions
9890            "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
9891
9892            // Higher-order functions
9893            "map" | "filter" | "reduce" | "singletonArray" |
9894
9895            // Date/time functions
9896            "now" | "millis" | "fromMillis" | "toMillis"
9897        )
9898    }
9899
9900    /// Call a built-in function directly with pre-evaluated Values
9901    /// This is used when passing built-in functions to higher-order functions like $map
9902    fn call_builtin_with_values(
9903        &mut self,
9904        name: &str,
9905        values: &[JValue],
9906    ) -> Result<JValue, EvaluatorError> {
9907        use crate::functions;
9908
9909        if values.is_empty() {
9910            return Err(EvaluatorError::EvaluationError(format!(
9911                "{}() requires at least 1 argument",
9912                name
9913            )));
9914        }
9915
9916        let arg = &values[0];
9917
9918        match name {
9919            "string" => Ok(functions::string::string(arg, None)?),
9920            "number" => Ok(functions::numeric::number(arg)?),
9921            "boolean" => Ok(functions::boolean::boolean(arg)?),
9922            "not" => {
9923                let b = functions::boolean::boolean(arg)?;
9924                match b {
9925                    JValue::Bool(val) => Ok(JValue::Bool(!val)),
9926                    _ => Err(EvaluatorError::TypeError(
9927                        "not() requires a boolean".to_string(),
9928                    )),
9929                }
9930            }
9931            "exists" => Ok(JValue::Bool(!arg.is_null())),
9932            "abs" => match arg {
9933                JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
9934                _ => Err(EvaluatorError::TypeError(
9935                    "abs() requires a number argument".to_string(),
9936                )),
9937            },
9938            "floor" => match arg {
9939                JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
9940                _ => Err(EvaluatorError::TypeError(
9941                    "floor() requires a number argument".to_string(),
9942                )),
9943            },
9944            "ceil" => match arg {
9945                JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
9946                _ => Err(EvaluatorError::TypeError(
9947                    "ceil() requires a number argument".to_string(),
9948                )),
9949            },
9950            "round" => match arg {
9951                JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
9952                _ => Err(EvaluatorError::TypeError(
9953                    "round() requires a number argument".to_string(),
9954                )),
9955            },
9956            "sqrt" => match arg {
9957                JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
9958                _ => Err(EvaluatorError::TypeError(
9959                    "sqrt() requires a number argument".to_string(),
9960                )),
9961            },
9962            "uppercase" => match arg {
9963                JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
9964                JValue::Null => Ok(JValue::Null),
9965                _ => Err(EvaluatorError::TypeError(
9966                    "uppercase() requires a string argument".to_string(),
9967                )),
9968            },
9969            "lowercase" => match arg {
9970                JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
9971                JValue::Null => Ok(JValue::Null),
9972                _ => Err(EvaluatorError::TypeError(
9973                    "lowercase() requires a string argument".to_string(),
9974                )),
9975            },
9976            "trim" => match arg {
9977                JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
9978                JValue::Null => Ok(JValue::Null),
9979                _ => Err(EvaluatorError::TypeError(
9980                    "trim() requires a string argument".to_string(),
9981                )),
9982            },
9983            "length" => match arg {
9984                JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
9985                JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
9986                JValue::Null => Ok(JValue::Null),
9987                _ => Err(EvaluatorError::TypeError(
9988                    "length() requires a string or array argument".to_string(),
9989                )),
9990            },
9991            "sum" => match arg {
9992                JValue::Array(arr) => {
9993                    let mut total = 0.0;
9994                    for item in arr.iter() {
9995                        match item {
9996                            JValue::Number(n) => {
9997                                total += *n;
9998                            }
9999                            _ => {
10000                                return Err(EvaluatorError::TypeError(
10001                                    "sum() requires all array elements to be numbers".to_string(),
10002                                ));
10003                            }
10004                        }
10005                    }
10006                    Ok(JValue::Number(total))
10007                }
10008                JValue::Number(n) => Ok(JValue::Number(*n)),
10009                JValue::Null => Ok(JValue::Null),
10010                _ => Err(EvaluatorError::TypeError(
10011                    "sum() requires an array of numbers".to_string(),
10012                )),
10013            },
10014            "count" => {
10015                match arg {
10016                    JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10017                    JValue::Null => Ok(JValue::Number(0.0)),
10018                    _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10019                }
10020            }
10021            "max" => match arg {
10022                JValue::Array(arr) => {
10023                    let mut max_val: Option<f64> = None;
10024                    for item in arr.iter() {
10025                        if let JValue::Number(n) = item {
10026                            let f = *n;
10027                            max_val = Some(max_val.map_or(f, |m| m.max(f)));
10028                        }
10029                    }
10030                    max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10031                }
10032                JValue::Number(n) => Ok(JValue::Number(*n)),
10033                JValue::Null => Ok(JValue::Null),
10034                _ => Err(EvaluatorError::TypeError(
10035                    "max() requires an array of numbers".to_string(),
10036                )),
10037            },
10038            "min" => match arg {
10039                JValue::Array(arr) => {
10040                    let mut min_val: Option<f64> = None;
10041                    for item in arr.iter() {
10042                        if let JValue::Number(n) = item {
10043                            let f = *n;
10044                            min_val = Some(min_val.map_or(f, |m| m.min(f)));
10045                        }
10046                    }
10047                    min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10048                }
10049                JValue::Number(n) => Ok(JValue::Number(*n)),
10050                JValue::Null => Ok(JValue::Null),
10051                _ => Err(EvaluatorError::TypeError(
10052                    "min() requires an array of numbers".to_string(),
10053                )),
10054            },
10055            "average" => match arg {
10056                JValue::Array(arr) => {
10057                    let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
10058                    if nums.is_empty() {
10059                        Ok(JValue::Null)
10060                    } else {
10061                        let avg = nums.iter().sum::<f64>() / nums.len() as f64;
10062                        Ok(JValue::Number(avg))
10063                    }
10064                }
10065                JValue::Number(n) => Ok(JValue::Number(*n)),
10066                JValue::Null => Ok(JValue::Null),
10067                _ => Err(EvaluatorError::TypeError(
10068                    "average() requires an array of numbers".to_string(),
10069                )),
10070            },
10071            "append" => {
10072                // append(array1, array2) - append second array to first
10073                if values.len() < 2 {
10074                    return Err(EvaluatorError::EvaluationError(
10075                        "append() requires 2 arguments".to_string(),
10076                    ));
10077                }
10078                let first = &values[0];
10079                let second = &values[1];
10080
10081                // Convert first to array if needed
10082                let mut result = match first {
10083                    JValue::Array(arr) => arr.to_vec(),
10084                    JValue::Null => vec![],
10085                    other => vec![other.clone()],
10086                };
10087
10088                // Append second (flatten if array)
10089                match second {
10090                    JValue::Array(arr) => result.extend(arr.iter().cloned()),
10091                    JValue::Null => {}
10092                    other => result.push(other.clone()),
10093                }
10094
10095                Ok(JValue::array(result))
10096            }
10097            "reverse" => match arg {
10098                JValue::Array(arr) => {
10099                    let mut reversed = arr.to_vec();
10100                    reversed.reverse();
10101                    Ok(JValue::array(reversed))
10102                }
10103                JValue::Null => Ok(JValue::Null),
10104                _ => Err(EvaluatorError::TypeError(
10105                    "reverse() requires an array".to_string(),
10106                )),
10107            },
10108            "keys" => match arg {
10109                JValue::Object(obj) => {
10110                    let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
10111                    Ok(JValue::array(keys))
10112                }
10113                JValue::Null => Ok(JValue::Null),
10114                _ => Err(EvaluatorError::TypeError(
10115                    "keys() requires an object".to_string(),
10116                )),
10117            },
10118
10119            // Add more functions as needed
10120            _ => Err(EvaluatorError::ReferenceError(format!(
10121                "Built-in function {} cannot be called with values directly",
10122                name
10123            ))),
10124        }
10125    }
10126
10127    /// Collect all descendant values recursively
10128    fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
10129        let mut descendants = Vec::new();
10130
10131        match value {
10132            JValue::Null => {
10133                // Null has no descendants, return empty
10134                return descendants;
10135            }
10136            JValue::Object(obj) => {
10137                // Include the current object
10138                descendants.push(value.clone());
10139
10140                for val in obj.values() {
10141                    // Recursively collect descendants
10142                    descendants.extend(self.collect_descendants(val));
10143                }
10144            }
10145            JValue::Array(arr) => {
10146                // DO NOT include the array itself - only recurse into elements
10147                // This matches JavaScript behavior: arrays are traversed but not collected
10148                for val in arr.iter() {
10149                    // Recursively collect descendants
10150                    descendants.extend(self.collect_descendants(val));
10151                }
10152            }
10153            _ => {
10154                // For primitives (string, number, boolean), just include the value itself
10155                descendants.push(value.clone());
10156            }
10157        }
10158
10159        descendants
10160    }
10161
10162    /// Evaluate a predicate (array filter or index)
10163    fn evaluate_predicate(
10164        &mut self,
10165        current: &JValue,
10166        predicate: &AstNode,
10167    ) -> Result<JValue, EvaluatorError> {
10168        // Special case: empty brackets [] (represented as Boolean(true))
10169        // This forces the value to be wrapped in an array
10170        if matches!(predicate, AstNode::Boolean(true)) {
10171            return match current {
10172                JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
10173                JValue::Null => Ok(JValue::Null),
10174                other => Ok(JValue::array(vec![other.clone()])),
10175            };
10176        }
10177
10178        match current {
10179            JValue::Array(_arr) => {
10180                // Standalone predicates do simple array operations (no mapping over sub-arrays)
10181
10182                // First, try to evaluate predicate as a simple number (array index)
10183                if let AstNode::Number(n) = predicate {
10184                    // Direct array indexing
10185                    return self.array_index(current, &JValue::Number(*n));
10186                }
10187
10188                // Fast path: if predicate is definitely a filter expression (comparison/logical),
10189                // skip speculative numeric evaluation and go directly to filter logic
10190                if Self::is_filter_predicate(predicate) {
10191                    // Try CompiledExpr fast path
10192                    if let Some(compiled) = try_compile_expr(predicate) {
10193                        let shape = _arr.first().and_then(build_shape_cache);
10194                        let mut filtered = Vec::with_capacity(_arr.len());
10195                        for item in _arr.iter() {
10196                            let result = if let Some(ref s) = shape {
10197                                eval_compiled_shaped(&compiled, item, None, s)?
10198                            } else {
10199                                eval_compiled(&compiled, item, None)?
10200                            };
10201                            if compiled_is_truthy(&result) {
10202                                filtered.push(item.clone());
10203                            }
10204                        }
10205                        return Ok(JValue::array(filtered));
10206                    }
10207                    // Fallback: full AST evaluation per element
10208                    let mut filtered = Vec::new();
10209                    for item in _arr.iter() {
10210                        let item_result = self.evaluate_internal(predicate, item)?;
10211                        if self.is_truthy(&item_result) {
10212                            filtered.push(item.clone());
10213                        }
10214                    }
10215                    return Ok(JValue::array(filtered));
10216                }
10217
10218                // Try to evaluate the predicate to see if it's a numeric index
10219                // If evaluation succeeds and yields a number, use it as an index
10220                // If evaluation fails (e.g., comparison error), treat as filter
10221                match self.evaluate_internal(predicate, current) {
10222                    Ok(JValue::Number(_)) => {
10223                        // It's a numeric index
10224                        let pred_result = self.evaluate_internal(predicate, current)?;
10225                        return self.array_index(current, &pred_result);
10226                    }
10227                    Ok(JValue::Array(indices)) => {
10228                        // Multiple array selectors [[indices]]
10229                        // Check if array contains any non-numeric values
10230                        let has_non_numeric =
10231                            indices.iter().any(|v| !matches!(v, JValue::Number(_)));
10232
10233                        if has_non_numeric {
10234                            // If array contains non-numeric values, return entire array
10235                            return Ok(current.clone());
10236                        }
10237
10238                        // Collect numeric indices, handling negative indices
10239                        let arr_len = _arr.len() as i64;
10240                        let mut resolved_indices: Vec<i64> = indices
10241                            .iter()
10242                            .filter_map(|v| {
10243                                if let JValue::Number(n) = v {
10244                                    let idx = *n as i64;
10245                                    // Resolve negative indices
10246                                    let actual_idx = if idx < 0 { arr_len + idx } else { idx };
10247                                    // Only include valid indices
10248                                    if actual_idx >= 0 && actual_idx < arr_len {
10249                                        Some(actual_idx)
10250                                    } else {
10251                                        None
10252                                    }
10253                                } else {
10254                                    None
10255                                }
10256                            })
10257                            .collect();
10258
10259                        // Sort and deduplicate indices
10260                        resolved_indices.sort();
10261                        resolved_indices.dedup();
10262
10263                        // Select elements at each sorted index
10264                        let result: Vec<JValue> = resolved_indices
10265                            .iter()
10266                            .map(|&idx| _arr[idx as usize].clone())
10267                            .collect();
10268
10269                        return Ok(JValue::array(result));
10270                    }
10271                    Ok(_) => {
10272                        // Evaluated successfully but not a number - might be a filter
10273                        // Fall through to filter logic
10274                    }
10275                    Err(_) => {
10276                        // Evaluation failed - it's likely a filter expression
10277                        // Fall through to filter logic
10278                    }
10279                }
10280
10281                // Try CompiledExpr fast path for filter expressions
10282                if let Some(compiled) = try_compile_expr(predicate) {
10283                    let shape = _arr.first().and_then(build_shape_cache);
10284                    let mut filtered = Vec::with_capacity(_arr.len());
10285                    for item in _arr.iter() {
10286                        let result = if let Some(ref s) = shape {
10287                            eval_compiled_shaped(&compiled, item, None, s)?
10288                        } else {
10289                            eval_compiled(&compiled, item, None)?
10290                        };
10291                        if compiled_is_truthy(&result) {
10292                            filtered.push(item.clone());
10293                        }
10294                    }
10295                    return Ok(JValue::array(filtered));
10296                }
10297
10298                // It's a filter expression - evaluate the predicate for each array element
10299                let mut filtered = Vec::new();
10300                for item in _arr.iter() {
10301                    let item_result = self.evaluate_internal(predicate, item)?;
10302
10303                    // If result is truthy, include this item
10304                    if self.is_truthy(&item_result) {
10305                        filtered.push(item.clone());
10306                    }
10307                }
10308
10309                Ok(JValue::array(filtered))
10310            }
10311            JValue::Object(obj) => {
10312                // For objects, predicate can be either:
10313                // 1. A string - property access (computed property name)
10314                // 2. A boolean expression - filter (return object if truthy)
10315                let pred_result = self.evaluate_internal(predicate, current)?;
10316
10317                // If it's a string, use it as a key for property access
10318                if let JValue::String(key) = &pred_result {
10319                    return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
10320                }
10321
10322                // Otherwise, treat as a filter expression
10323                // If the predicate is truthy, return the object; otherwise return undefined
10324                if self.is_truthy(&pred_result) {
10325                    Ok(current.clone())
10326                } else {
10327                    Ok(JValue::Undefined)
10328                }
10329            }
10330            _ => {
10331                // For primitive values (string, number, boolean):
10332                // In JSONata, scalars are treated as single-element arrays when indexed.
10333                // So value[0] returns value, value[1] returns undefined.
10334
10335                // First check if predicate is a numeric literal
10336                if let AstNode::Number(n) = predicate {
10337                    // For scalars, index 0 or -1 returns the value, others return undefined
10338                    let idx = n.floor() as i64;
10339                    if idx == 0 || idx == -1 {
10340                        return Ok(current.clone());
10341                    } else {
10342                        return Ok(JValue::Undefined);
10343                    }
10344                }
10345
10346                // Try to evaluate the predicate to see if it's a numeric index
10347                let pred_result = self.evaluate_internal(predicate, current)?;
10348
10349                if let JValue::Number(n) = &pred_result {
10350                    // It's a numeric index - treat scalar as single-element array
10351                    let idx = n.floor() as i64;
10352                    if idx == 0 || idx == -1 {
10353                        return Ok(current.clone());
10354                    } else {
10355                        return Ok(JValue::Undefined);
10356                    }
10357                }
10358
10359                // For non-numeric predicates, treat as a filter:
10360                // value[true] returns value, value[false] returns undefined
10361                // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
10362                if self.is_truthy(&pred_result) {
10363                    Ok(current.clone())
10364                } else {
10365                    // Return undefined (not null) so $map can filter it out
10366                    Ok(JValue::Undefined)
10367                }
10368            }
10369        }
10370    }
10371
10372    /// Evaluate a sort term expression, distinguishing missing fields from explicit null
10373    /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
10374    fn evaluate_sort_term(
10375        &mut self,
10376        term_expr: &AstNode,
10377        element: &JValue,
10378    ) -> Result<JValue, EvaluatorError> {
10379        // For tuples (from index binding), extract the actual value from @ field
10380        let actual_element = if let JValue::Object(obj) = element {
10381            if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
10382                obj.get("@").cloned().unwrap_or(JValue::Null)
10383            } else {
10384                element.clone()
10385            }
10386        } else {
10387            element.clone()
10388        };
10389
10390        // For simple field access (Path with single Name step), check if field exists
10391        if let AstNode::Path { steps } = term_expr {
10392            if steps.len() == 1 && steps[0].stages.is_empty() {
10393                if let AstNode::Name(field_name) = &steps[0].node {
10394                    // Check if the field exists in the element
10395                    if let JValue::Object(obj) = &actual_element {
10396                        return match obj.get(field_name) {
10397                            Some(val) => Ok(val.clone()),  // Field exists (may be null)
10398                            None => Ok(JValue::Undefined), // Field is missing
10399                        };
10400                    } else {
10401                        // Not an object - return undefined
10402                        return Ok(JValue::Undefined);
10403                    }
10404                }
10405            }
10406        }
10407
10408        // For complex expressions, evaluate against the tuple's `@` value (the
10409        // real element), not the wrapper. The tuple's carried focus/index/ancestor
10410        // bindings are reachable via context (bound by evaluate_sort), so a term
10411        // like `$`, `%.Price`, or `$pos` still resolves correctly.
10412        let result = self.evaluate_internal(term_expr, &actual_element)?;
10413
10414        // If the result is null from a complex expression, we can't easily tell if it's
10415        // "missing field" or "explicit null". For now, treat null results as undefined
10416        // to maintain compatibility with existing tests.
10417        // TODO: For full JS compatibility, would need deeper analysis of the expression
10418        if result.is_null() {
10419            return Ok(JValue::Undefined);
10420        }
10421
10422        Ok(result)
10423    }
10424
10425    /// Evaluate sort operator
10426    fn evaluate_sort(
10427        &mut self,
10428        data: &JValue,
10429        terms: &[(AstNode, bool)],
10430    ) -> Result<JValue, EvaluatorError> {
10431        // If data is null, return null
10432        if data.is_null() {
10433            return Ok(JValue::Null);
10434        }
10435
10436        // If data is not an array, return it as-is (can't sort a single value)
10437        let array = match data {
10438            JValue::Array(arr) => arr.clone(),
10439            other => return Ok(other.clone()),
10440        };
10441
10442        // If empty array, return as-is
10443        if array.is_empty() {
10444            return Ok(JValue::Array(array));
10445        }
10446
10447        // Evaluate sort keys for each element
10448        let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
10449
10450        for (idx, element) in array.iter().enumerate() {
10451            let mut sort_keys = Vec::new();
10452
10453            // When sorting a tuple stream (the input path had a `%`/`@`/`#`
10454            // step, so each element is a `{@, !label, $var, __tuple__}`
10455            // wrapper), bind its carried ancestor/focus/index keys into scope
10456            // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
10457            // create_tuple_stream's per-tuple frame binding. Sort terms attach
10458            // to a synthetic step after the last input step, so `%` refers to
10459            // the last input step's ancestry, carried under `!label` here.
10460            // Saves/restores rather than blindly unbinding, so a tuple key
10461            // that collides with a live outer `:=` binding doesn't get
10462            // deleted once this row's sort terms are evaluated.
10463            let tuple_bindings = match element {
10464                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
10465                    Some(self.bind_tuple_keys(obj))
10466                }
10467                _ => None,
10468            };
10469
10470            // When sorting a tuple stream, `$` and the term's data context are the
10471            // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
10472            // otherwise a term like `^($)` would try to order by the wrapper
10473            // object and raise T2008. The carried focus/index/ancestor keys stay
10474            // reachable via the context bindings established just above.
10475            let term_data = match element {
10476                JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
10477                    obj.get("@").cloned().unwrap_or(JValue::Null)
10478                }
10479                other => other.clone(),
10480            };
10481
10482            // Evaluate each sort term with $ bound to the element
10483            for (term_expr, _ascending) in terms {
10484                // Save current $ binding
10485                let saved_dollar = self.context.lookup("$").cloned();
10486
10487                // Bind $ to current element
10488                self.context.bind("$".to_string(), term_data.clone());
10489
10490                // Evaluate the sort expression, distinguishing missing fields from explicit null
10491                let sort_value = self.evaluate_sort_term(term_expr, element)?;
10492
10493                // Restore $ binding
10494                if let Some(val) = saved_dollar {
10495                    self.context.bind("$".to_string(), val);
10496                } else {
10497                    self.context.unbind("$");
10498                }
10499
10500                sort_keys.push(sort_value);
10501            }
10502
10503            if let Some(tuple_bindings) = tuple_bindings {
10504                tuple_bindings.restore(self);
10505            }
10506
10507            indexed_array.push((idx, sort_keys));
10508        }
10509
10510        // Validate that all sort keys are comparable (same type, or undefined)
10511        // Undefined values (missing fields) are allowed and sort to the end
10512        // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
10513        for term_idx in 0..terms.len() {
10514            let mut first_valid_type: Option<&str> = None;
10515
10516            for (_idx, sort_keys) in &indexed_array {
10517                let sort_value = &sort_keys[term_idx];
10518
10519                // Skip undefined markers (missing fields) - these are allowed and sort to end
10520                if sort_value.is_undefined() {
10521                    continue;
10522                }
10523
10524                // Get the type name for this value
10525                // Note: explicit null is NOT allowed - typeof null === 'object' in JS
10526                let value_type = match sort_value {
10527                    JValue::Number(_) => "number",
10528                    JValue::String(_) => "string",
10529                    JValue::Bool(_) => "boolean",
10530                    JValue::Array(_) => "array",
10531                    JValue::Object(_) => "object", // This catches non-undefined objects
10532                    JValue::Null => "null",        // Explicit null from data
10533                    _ => "unknown",
10534                };
10535
10536                // Check that sort keys are only numbers or strings
10537                // Null, boolean, array, and object types are not valid for sorting
10538                if value_type != "number" && value_type != "string" {
10539                    return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
10540                }
10541
10542                // Check if this matches the first valid type we saw
10543                if let Some(first_type) = first_valid_type {
10544                    if first_type != value_type {
10545                        return Err(EvaluatorError::TypeError(format!(
10546                            "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
10547                            first_type, value_type
10548                        )));
10549                    }
10550                } else {
10551                    first_valid_type = Some(value_type);
10552                }
10553            }
10554        }
10555
10556        // Sort the indexed array
10557        indexed_array.sort_by(|a, b| {
10558            // Compare sort keys in order
10559            for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
10560                let left = &a.1[i];
10561                let right = &b.1[i];
10562
10563                let cmp = self.compare_values(left, right);
10564
10565                if cmp != std::cmp::Ordering::Equal {
10566                    return if *ascending { cmp } else { cmp.reverse() };
10567                }
10568            }
10569
10570            // If all keys are equal, maintain original order (stable sort)
10571            a.0.cmp(&b.0)
10572        });
10573
10574        // Extract sorted elements
10575        let sorted: Vec<JValue> = indexed_array
10576            .iter()
10577            .map(|(idx, _)| array[*idx].clone())
10578            .collect();
10579
10580        Ok(JValue::array(sorted))
10581    }
10582
10583    /// Compare two values for sorting (JSONata semantics)
10584    fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
10585        // Handle undefined markers first - they sort to the end
10586        let left_undef = left.is_undefined();
10587        let right_undef = right.is_undefined();
10588
10589        if left_undef && right_undef {
10590            return Ordering::Equal;
10591        }
10592        if left_undef {
10593            return Ordering::Greater; // Undefined sorts last
10594        }
10595        if right_undef {
10596            return Ordering::Less;
10597        }
10598
10599        match (left, right) {
10600            // Nulls also sort last (explicit null in data)
10601            (JValue::Null, JValue::Null) => Ordering::Equal,
10602            (JValue::Null, _) => Ordering::Greater,
10603            (_, JValue::Null) => Ordering::Less,
10604
10605            // Numbers
10606            (JValue::Number(a), JValue::Number(b)) => {
10607                let a_f64 = *a;
10608                let b_f64 = *b;
10609                a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
10610            }
10611
10612            // Strings
10613            (JValue::String(a), JValue::String(b)) => a.cmp(b),
10614
10615            // Booleans
10616            (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
10617
10618            // Arrays (lexicographic comparison)
10619            (JValue::Array(a), JValue::Array(b)) => {
10620                for (a_elem, b_elem) in a.iter().zip(b.iter()) {
10621                    let cmp = self.compare_values(a_elem, b_elem);
10622                    if cmp != Ordering::Equal {
10623                        return cmp;
10624                    }
10625                }
10626                a.len().cmp(&b.len())
10627            }
10628
10629            // Different types: use type ordering
10630            // null < bool < number < string < array < object
10631            (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
10632            (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
10633            (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
10634            (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
10635
10636            (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
10637            (JValue::Number(_), JValue::String(_)) => Ordering::Less,
10638            (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
10639            (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
10640
10641            (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
10642            (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
10643            (JValue::String(_), JValue::Array(_)) => Ordering::Less,
10644            (JValue::String(_), JValue::Object(_)) => Ordering::Less,
10645
10646            (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
10647            (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
10648            (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
10649            (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
10650
10651            (JValue::Object(_), _) => Ordering::Greater,
10652            _ => Ordering::Equal,
10653        }
10654    }
10655
10656    /// Check if a value is truthy (JSONata semantics).
10657    fn is_truthy(&self, value: &JValue) -> bool {
10658        match value {
10659            JValue::Null | JValue::Undefined => false,
10660            JValue::Bool(b) => *b,
10661            JValue::Number(n) => *n != 0.0,
10662            JValue::String(s) => !s.is_empty(),
10663            JValue::Array(arr) => !arr.is_empty(),
10664            JValue::Object(obj) => !obj.is_empty(),
10665            _ => false,
10666        }
10667    }
10668
10669    /// Check if a value is truthy for the default operator (?:)
10670    /// This has special semantics:
10671    /// - Lambda/function objects are not values, so they're falsy
10672    /// - Arrays containing only falsy elements are falsy
10673    /// - Otherwise, use standard truthiness
10674    fn is_truthy_for_default(&self, value: &JValue) -> bool {
10675        match value {
10676            // Lambda/function values are not data values, so they're falsy
10677            JValue::Lambda { .. } | JValue::Builtin { .. } => false,
10678            // Arrays need special handling - check if all elements are falsy
10679            JValue::Array(arr) => {
10680                if arr.is_empty() {
10681                    return false;
10682                }
10683                // Array is truthy only if it contains at least one truthy element
10684                arr.iter().any(|elem| self.is_truthy(elem))
10685            }
10686            // For all other types, use standard truthiness
10687            _ => self.is_truthy(value),
10688        }
10689    }
10690
10691    /// Unwrap singleton arrays to scalar values
10692    /// This is used when no explicit array-keeping operation (like []) was used
10693    fn unwrap_singleton(&self, value: JValue) -> JValue {
10694        match value {
10695            JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
10696            _ => value,
10697        }
10698    }
10699
10700    /// Extract lambda IDs from a value (used for closure preservation)
10701    /// Finds any lambda_id references in the value so they can be preserved
10702    /// when exiting a block scope
10703    fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
10704        // Fast path: scalars can never contain lambda references
10705        match value {
10706            JValue::Number(_)
10707            | JValue::Bool(_)
10708            | JValue::String(_)
10709            | JValue::Null
10710            | JValue::Undefined
10711            | JValue::Regex { .. }
10712            | JValue::Builtin { .. } => return Vec::new(),
10713            _ => {}
10714        }
10715        let mut ids = Vec::new();
10716        self.collect_lambda_ids(value, &mut ids);
10717        ids
10718    }
10719
10720    fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
10721        match value {
10722            JValue::Lambda { lambda_id, .. } => {
10723                let id_str = lambda_id.to_string();
10724                if !ids.contains(&id_str) {
10725                    ids.push(id_str);
10726                    // Transitively follow the stored lambda's captured_env
10727                    // to find all referenced lambdas. This is critical for
10728                    // closures like the Y-combinator where returned lambdas
10729                    // capture other lambdas in their environment.
10730                    if let Some(stored) = self.context.lookup_lambda(lambda_id) {
10731                        let env_values: Vec<JValue> =
10732                            stored.captured_env.values().cloned().collect();
10733                        for env_value in &env_values {
10734                            self.collect_lambda_ids(env_value, ids);
10735                        }
10736                    }
10737                }
10738            }
10739            JValue::Object(map) => {
10740                // Recurse into object values
10741                for v in map.values() {
10742                    self.collect_lambda_ids(v, ids);
10743                }
10744            }
10745            JValue::Array(arr) => {
10746                // Recurse into array elements
10747                for v in arr.iter() {
10748                    self.collect_lambda_ids(v, ids);
10749                }
10750            }
10751            _ => {}
10752        }
10753    }
10754
10755    /// Equality comparison (JSONata semantics)
10756    fn equals(&self, left: &JValue, right: &JValue) -> bool {
10757        crate::functions::array::values_equal(left, right)
10758    }
10759
10760    /// Addition
10761    fn add(
10762        &self,
10763        left: &JValue,
10764        right: &JValue,
10765        left_is_explicit_null: bool,
10766        right_is_explicit_null: bool,
10767    ) -> Result<JValue, EvaluatorError> {
10768        match (left, right) {
10769            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
10770            // Explicit null literal with number -> T2002 error
10771            (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
10772                Err(EvaluatorError::TypeError(
10773                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
10774                ))
10775            }
10776            (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
10777                Err(EvaluatorError::TypeError(
10778                    "T2002: The right side of the + operator must evaluate to a number".to_string(),
10779                ))
10780            }
10781            (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
10782                Err(EvaluatorError::TypeError(
10783                    "T2002: The left side of the + operator must evaluate to a number".to_string(),
10784                ))
10785            }
10786            // Undefined variable (null/undefined) with number -> undefined result
10787            (JValue::Null | JValue::Undefined, JValue::Number(_))
10788            | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
10789            // Boolean with anything (including undefined) -> T2001 error
10790            (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
10791                "T2001: The left side of the '+' operator must evaluate to a number or a string"
10792                    .to_string(),
10793            )),
10794            (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
10795                "T2001: The right side of the '+' operator must evaluate to a number or a string"
10796                    .to_string(),
10797            )),
10798            // Undefined with undefined -> undefined
10799            (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
10800                Ok(JValue::Null)
10801            }
10802            _ => Err(EvaluatorError::TypeError(format!(
10803                "Cannot add {:?} and {:?}",
10804                left, right
10805            ))),
10806        }
10807    }
10808
10809    /// Subtraction
10810    fn subtract(
10811        &self,
10812        left: &JValue,
10813        right: &JValue,
10814        left_is_explicit_null: bool,
10815        right_is_explicit_null: bool,
10816    ) -> Result<JValue, EvaluatorError> {
10817        match (left, right) {
10818            (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
10819            // Explicit null literal -> error
10820            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10821                "T2002: The left side of the - operator must evaluate to a number".to_string(),
10822            )),
10823            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10824                "T2002: The right side of the - operator must evaluate to a number".to_string(),
10825            )),
10826            // Undefined variables -> undefined result
10827            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10828                Ok(JValue::Null)
10829            }
10830            _ => Err(EvaluatorError::TypeError(format!(
10831                "Cannot subtract {:?} and {:?}",
10832                left, right
10833            ))),
10834        }
10835    }
10836
10837    /// Multiplication
10838    fn multiply(
10839        &self,
10840        left: &JValue,
10841        right: &JValue,
10842        left_is_explicit_null: bool,
10843        right_is_explicit_null: bool,
10844    ) -> Result<JValue, EvaluatorError> {
10845        match (left, right) {
10846            (JValue::Number(a), JValue::Number(b)) => {
10847                let result = *a * *b;
10848                // Check for overflow to Infinity
10849                if result.is_infinite() {
10850                    return Err(EvaluatorError::EvaluationError(
10851                        "D1001: Number out of range".to_string(),
10852                    ));
10853                }
10854                Ok(JValue::Number(result))
10855            }
10856            // Explicit null literal -> error
10857            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10858                "T2002: The left side of the * operator must evaluate to a number".to_string(),
10859            )),
10860            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10861                "T2002: The right side of the * operator must evaluate to a number".to_string(),
10862            )),
10863            // Undefined variables -> undefined result
10864            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10865                Ok(JValue::Null)
10866            }
10867            _ => Err(EvaluatorError::TypeError(format!(
10868                "Cannot multiply {:?} and {:?}",
10869                left, right
10870            ))),
10871        }
10872    }
10873
10874    /// Division
10875    fn divide(
10876        &self,
10877        left: &JValue,
10878        right: &JValue,
10879        left_is_explicit_null: bool,
10880        right_is_explicit_null: bool,
10881    ) -> Result<JValue, EvaluatorError> {
10882        match (left, right) {
10883            (JValue::Number(a), JValue::Number(b)) => {
10884                let denominator = *b;
10885                if denominator == 0.0 {
10886                    return Err(EvaluatorError::EvaluationError(
10887                        "Division by zero".to_string(),
10888                    ));
10889                }
10890                Ok(JValue::Number(*a / denominator))
10891            }
10892            // Explicit null literal -> error
10893            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10894                "T2002: The left side of the / operator must evaluate to a number".to_string(),
10895            )),
10896            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10897                "T2002: The right side of the / operator must evaluate to a number".to_string(),
10898            )),
10899            // Undefined variables -> undefined result
10900            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10901                Ok(JValue::Null)
10902            }
10903            _ => Err(EvaluatorError::TypeError(format!(
10904                "Cannot divide {:?} and {:?}",
10905                left, right
10906            ))),
10907        }
10908    }
10909
10910    /// Modulo
10911    fn modulo(
10912        &self,
10913        left: &JValue,
10914        right: &JValue,
10915        left_is_explicit_null: bool,
10916        right_is_explicit_null: bool,
10917    ) -> Result<JValue, EvaluatorError> {
10918        match (left, right) {
10919            (JValue::Number(a), JValue::Number(b)) => {
10920                let denominator = *b;
10921                if denominator == 0.0 {
10922                    return Err(EvaluatorError::EvaluationError(
10923                        "Division by zero".to_string(),
10924                    ));
10925                }
10926                Ok(JValue::Number(*a % denominator))
10927            }
10928            // Explicit null literal -> error
10929            (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10930                "T2002: The left side of the % operator must evaluate to a number".to_string(),
10931            )),
10932            (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10933                "T2002: The right side of the % operator must evaluate to a number".to_string(),
10934            )),
10935            // Undefined variables -> undefined result
10936            (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10937                Ok(JValue::Null)
10938            }
10939            _ => Err(EvaluatorError::TypeError(format!(
10940                "Cannot compute modulo of {:?} and {:?}",
10941                left, right
10942            ))),
10943        }
10944    }
10945
10946    /// Get human-readable type name for error messages
10947    fn type_name(value: &JValue) -> &'static str {
10948        match value {
10949            JValue::Null => "null",
10950            JValue::Bool(_) => "boolean",
10951            JValue::Number(_) => "number",
10952            JValue::String(_) => "string",
10953            JValue::Array(_) => "array",
10954            JValue::Object(_) => "object",
10955            _ => "unknown",
10956        }
10957    }
10958
10959    /// Ordered comparison with null/type checking shared across <, <=, >, >=
10960    ///
10961    /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
10962    /// `compare_strs` receives (left_str, right_str) for string operands.
10963    /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
10964    fn ordered_compare(
10965        &self,
10966        left: &JValue,
10967        right: &JValue,
10968        left_is_explicit_null: bool,
10969        right_is_explicit_null: bool,
10970        op_symbol: &str,
10971        compare_nums: fn(f64, f64) -> bool,
10972        compare_strs: fn(&str, &str) -> bool,
10973    ) -> Result<JValue, EvaluatorError> {
10974        match (left, right) {
10975            (JValue::Number(a), JValue::Number(b)) => {
10976                Ok(JValue::Bool(compare_nums(*a, *b)))
10977            }
10978            (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
10979            // Both null/undefined -> return undefined
10980            (JValue::Null, JValue::Null) => Ok(JValue::Null),
10981            // Explicit null literal with any type (except null) -> T2010 error
10982            (JValue::Null, _) if left_is_explicit_null => {
10983                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10984            }
10985            (_, JValue::Null) if right_is_explicit_null => {
10986                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10987            }
10988            // Boolean with undefined -> T2010 error
10989            (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
10990                Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10991            }
10992            // Number or String with undefined (not explicit null) -> undefined result
10993            (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
10994            (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
10995                Ok(JValue::Null)
10996            }
10997            // String vs Number -> T2009
10998            (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
10999                Err(EvaluatorError::EvaluationError(format!(
11000                    "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
11001                    op_symbol
11002                )))
11003            }
11004            // Boolean comparisons -> T2010
11005            (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
11006                Err(EvaluatorError::EvaluationError(format!(
11007                    "T2010: Cannot compare {} and {}",
11008                    Self::type_name(left), Self::type_name(right)
11009                )))
11010            }
11011            // Other type mismatches
11012            _ => Err(EvaluatorError::EvaluationError(format!(
11013                "T2010: Cannot compare {} and {}",
11014                Self::type_name(left), Self::type_name(right)
11015            ))),
11016        }
11017    }
11018
11019    /// Less than comparison
11020    fn less_than(
11021        &self,
11022        left: &JValue,
11023        right: &JValue,
11024        left_is_explicit_null: bool,
11025        right_is_explicit_null: bool,
11026    ) -> Result<JValue, EvaluatorError> {
11027        self.ordered_compare(
11028            left,
11029            right,
11030            left_is_explicit_null,
11031            right_is_explicit_null,
11032            "<",
11033            |a, b| a < b,
11034            |a, b| a < b,
11035        )
11036    }
11037
11038    /// Less than or equal comparison
11039    fn less_than_or_equal(
11040        &self,
11041        left: &JValue,
11042        right: &JValue,
11043        left_is_explicit_null: bool,
11044        right_is_explicit_null: bool,
11045    ) -> Result<JValue, EvaluatorError> {
11046        self.ordered_compare(
11047            left,
11048            right,
11049            left_is_explicit_null,
11050            right_is_explicit_null,
11051            "<=",
11052            |a, b| a <= b,
11053            |a, b| a <= b,
11054        )
11055    }
11056
11057    /// Greater than comparison
11058    fn greater_than(
11059        &self,
11060        left: &JValue,
11061        right: &JValue,
11062        left_is_explicit_null: bool,
11063        right_is_explicit_null: bool,
11064    ) -> Result<JValue, EvaluatorError> {
11065        self.ordered_compare(
11066            left,
11067            right,
11068            left_is_explicit_null,
11069            right_is_explicit_null,
11070            ">",
11071            |a, b| a > b,
11072            |a, b| a > b,
11073        )
11074    }
11075
11076    /// Greater than or equal comparison
11077    fn greater_than_or_equal(
11078        &self,
11079        left: &JValue,
11080        right: &JValue,
11081        left_is_explicit_null: bool,
11082        right_is_explicit_null: bool,
11083    ) -> Result<JValue, EvaluatorError> {
11084        self.ordered_compare(
11085            left,
11086            right,
11087            left_is_explicit_null,
11088            right_is_explicit_null,
11089            ">=",
11090            |a, b| a >= b,
11091            |a, b| a >= b,
11092        )
11093    }
11094
11095    /// Convert a value to a string for concatenation
11096    fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
11097        match value {
11098            JValue::String(s) => Ok(s.to_string()),
11099            JValue::Null => Ok(String::new()),
11100            JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
11101                match crate::functions::string::string(value, None) {
11102                    Ok(JValue::String(s)) => Ok(s.to_string()),
11103                    Ok(JValue::Null) => Ok(String::new()),
11104                    _ => Err(EvaluatorError::TypeError(
11105                        "Cannot concatenate complex types".to_string(),
11106                    )),
11107                }
11108            }
11109            _ => Ok(String::new()),
11110        }
11111    }
11112
11113    /// String concatenation
11114    fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11115        let left_str = Self::value_to_concat_string(left)?;
11116        let right_str = Self::value_to_concat_string(right)?;
11117        Ok(JValue::string(format!("{}{}", left_str, right_str)))
11118    }
11119
11120    /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
11121    fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11122        // Check left operand is a number or null
11123        let start_f64 = match left {
11124            JValue::Number(n) => Some(*n),
11125            JValue::Null | JValue::Undefined => None,
11126            _ => {
11127                return Err(EvaluatorError::EvaluationError(
11128                    "T2003: Left operand of range operator must be a number".to_string(),
11129                ));
11130            }
11131        };
11132
11133        // Check left operand is an integer (if it's a number)
11134        if let Some(val) = start_f64 {
11135            if val.fract() != 0.0 {
11136                return Err(EvaluatorError::EvaluationError(
11137                    "T2003: Left operand of range operator must be an integer".to_string(),
11138                ));
11139            }
11140        }
11141
11142        // Check right operand is a number or null
11143        let end_f64 = match right {
11144            JValue::Number(n) => Some(*n),
11145            JValue::Null | JValue::Undefined => None,
11146            _ => {
11147                return Err(EvaluatorError::EvaluationError(
11148                    "T2004: Right operand of range operator must be a number".to_string(),
11149                ));
11150            }
11151        };
11152
11153        // Check right operand is an integer (if it's a number)
11154        if let Some(val) = end_f64 {
11155            if val.fract() != 0.0 {
11156                return Err(EvaluatorError::EvaluationError(
11157                    "T2004: Right operand of range operator must be an integer".to_string(),
11158                ));
11159            }
11160        }
11161
11162        // If either operand is null, return empty array
11163        if start_f64.is_none() || end_f64.is_none() {
11164            return Ok(JValue::array(vec![]));
11165        }
11166
11167        let start = start_f64.unwrap() as i64;
11168        let end = end_f64.unwrap() as i64;
11169
11170        // Check range size limit (10 million elements max)
11171        let size = if start <= end {
11172            (end - start + 1) as usize
11173        } else {
11174            0
11175        };
11176        if size > 10_000_000 {
11177            return Err(EvaluatorError::EvaluationError(
11178                "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
11179            ));
11180        }
11181
11182        let mut result = Vec::with_capacity(size);
11183        if start <= end {
11184            for i in start..=end {
11185                result.push(JValue::Number(i as f64));
11186            }
11187        }
11188        // Note: if start > end, return empty array (not reversed)
11189        Ok(JValue::array(result))
11190    }
11191
11192    /// In operator (checks if left is in right array/object)
11193    /// Array indexing: array[index]
11194    fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
11195        match (array, index) {
11196            (JValue::Array(arr), JValue::Number(n)) => {
11197                let idx = *n as i64;
11198                let len = arr.len() as i64;
11199
11200                // Handle negative indexing (offset from end)
11201                let actual_idx = if idx < 0 { len + idx } else { idx };
11202
11203                if actual_idx < 0 || actual_idx >= len {
11204                    Ok(JValue::Undefined)
11205                } else {
11206                    Ok(arr[actual_idx as usize].clone())
11207                }
11208            }
11209            _ => Err(EvaluatorError::TypeError(
11210                "Array indexing requires array and number".to_string(),
11211            )),
11212        }
11213    }
11214
11215    /// Array filtering: array[predicate]
11216    /// Evaluates the predicate for each item in the array and returns items where predicate is true
11217    fn array_filter(
11218        &mut self,
11219        _lhs_node: &AstNode,
11220        rhs_node: &AstNode,
11221        array: &JValue,
11222        _original_data: &JValue,
11223    ) -> Result<JValue, EvaluatorError> {
11224        match array {
11225            JValue::Array(arr) => {
11226                // Pre-allocate with estimated capacity (assume ~50% will match)
11227                let mut filtered = Vec::with_capacity(arr.len() / 2);
11228
11229                for item in arr.iter() {
11230                    // Evaluate the predicate in the context of this array item
11231                    // The item becomes the new "current context" ($)
11232                    let predicate_result = self.evaluate_internal(rhs_node, item)?;
11233
11234                    // Check if the predicate is truthy
11235                    if self.is_truthy(&predicate_result) {
11236                        filtered.push(item.clone());
11237                    }
11238                }
11239
11240                Ok(JValue::array(filtered))
11241            }
11242            _ => Err(EvaluatorError::TypeError(
11243                "Array filtering requires an array".to_string(),
11244            )),
11245        }
11246    }
11247
11248    fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11249        // If either side is undefined/null, return false (not an error)
11250        // This matches JavaScript behavior
11251        if left.is_null() || right.is_null() {
11252            return Ok(JValue::Bool(false));
11253        }
11254
11255        match right {
11256            JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
11257            JValue::Object(obj) => {
11258                if let JValue::String(key) = left {
11259                    Ok(JValue::Bool(obj.contains_key(&**key)))
11260                } else {
11261                    Ok(JValue::Bool(false))
11262                }
11263            }
11264            // If right side is not an array or object (e.g., string, number),
11265            // wrap it in an array for comparison
11266            other => Ok(JValue::Bool(self.equals(left, other))),
11267        }
11268    }
11269
11270    /// Create a partially applied function from a function call with placeholder arguments
11271    /// This evaluates non-placeholder arguments and creates a new lambda that takes
11272    /// the placeholder positions as parameters.
11273    fn create_partial_application(
11274        &mut self,
11275        name: &str,
11276        args: &[AstNode],
11277        is_builtin: bool,
11278        data: &JValue,
11279    ) -> Result<JValue, EvaluatorError> {
11280        // First, look up the function to ensure it exists
11281        let is_lambda = self.context.lookup_lambda(name).is_some()
11282            || (self
11283                .context
11284                .lookup(name)
11285                .map(|v| matches!(v, JValue::Lambda { .. }))
11286                .unwrap_or(false));
11287
11288        // Built-in functions must be called with $ prefix for partial application
11289        // Without $, it's an error (T1007) suggesting the user forgot the $
11290        if !is_lambda && !is_builtin {
11291            // Check if it's a built-in function called without $
11292            if self.is_builtin_function(name) {
11293                return Err(EvaluatorError::EvaluationError(format!(
11294                    "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
11295                    name
11296                )));
11297            }
11298            return Err(EvaluatorError::EvaluationError(
11299                "T1008: Attempted to partially apply a non-function".to_string(),
11300            ));
11301        }
11302
11303        // Evaluate non-placeholder arguments and track placeholder positions
11304        let mut bound_args: Vec<(usize, JValue)> = Vec::new();
11305        let mut placeholder_positions: Vec<usize> = Vec::new();
11306
11307        for (i, arg) in args.iter().enumerate() {
11308            if matches!(arg, AstNode::Placeholder) {
11309                placeholder_positions.push(i);
11310            } else {
11311                let value = self.evaluate_internal(arg, data)?;
11312                bound_args.push((i, value));
11313            }
11314        }
11315
11316        // Generate parameter names for each placeholder
11317        let param_names: Vec<String> = placeholder_positions
11318            .iter()
11319            .enumerate()
11320            .map(|(i, _)| format!("__p{}", i))
11321            .collect();
11322
11323        // Store the partial application info as a special lambda
11324        // When invoked, it will call the original function with bound + placeholder args
11325        let partial_id = format!(
11326            "__partial_{}_{}_{}",
11327            name,
11328            placeholder_positions.len(),
11329            bound_args.len()
11330        );
11331
11332        // Create a stored lambda that represents this partial application
11333        // The body is a marker that we'll interpret specially during invocation
11334        let stored_lambda = StoredLambda {
11335            params: param_names.clone(),
11336            body: AstNode::String(format!(
11337                "__partial_call:{}:{}:{}",
11338                name,
11339                is_builtin,
11340                args.len()
11341            )),
11342            compiled_body: None, // Partial application uses a special body marker
11343            signature: None,
11344            captured_env: {
11345                let mut env = self.capture_current_environment();
11346                // Store the bound arguments in the captured environment
11347                for (pos, value) in &bound_args {
11348                    env.insert(format!("__bound_arg_{}", pos), value.clone());
11349                }
11350                // Store placeholder positions
11351                env.insert(
11352                    "__placeholder_positions".to_string(),
11353                    JValue::array(
11354                        placeholder_positions
11355                            .iter()
11356                            .map(|p| JValue::Number(*p as f64))
11357                            .collect::<Vec<_>>(),
11358                    ),
11359                );
11360                // Store total argument count
11361                env.insert(
11362                    "__total_args".to_string(),
11363                    JValue::Number(args.len() as f64),
11364                );
11365                env
11366            },
11367            captured_data: Some(data.clone()),
11368            thunk: false,
11369        };
11370
11371        self.context.bind_lambda(partial_id.clone(), stored_lambda);
11372
11373        // Return a lambda object that can be invoked
11374        let lambda_obj = JValue::lambda(
11375            partial_id.as_str(),
11376            param_names,
11377            Some(name.to_string()),
11378            None::<String>,
11379        );
11380
11381        Ok(lambda_obj)
11382    }
11383}
11384
11385impl Default for Evaluator {
11386    fn default() -> Self {
11387        Self::new()
11388    }
11389}
11390
11391#[cfg(test)]
11392mod tests {
11393    use super::*;
11394    use crate::ast::{BinaryOp, UnaryOp};
11395
11396    // --- Task 7: tuple-wrapper output leak -----------------------------------
11397    //
11398    // `%`/`@`/`#` are implemented internally via a tuple-stream representation
11399    // (`create_tuple_stream`): each element gets wrapped as
11400    // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
11401    // consume/re-wrap these, but the *final* evaluate() result can still carry
11402    // a lingering wrapper -- confirmed for real by dumping actual output before
11403    // this fix (see task-7-report.md for the raw before/after). These tests
11404    // pin both the bare top-level case (Task 5's brief `#` example) and the
11405    // object/array-construction-nested case (found while verifying the brief's
11406    // illustrative fix against real output -- a plain per-element Array-only
11407    // recursion does not reach into a constructed object's field values).
11408
11409    fn dataset5_for_tuple_tests() -> JValue {
11410        let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
11411        serde_json::from_str::<serde_json::Value>(s).unwrap().into()
11412    }
11413
11414    fn assert_no_tuple_wrapper(value: &JValue) {
11415        match value {
11416            JValue::Object(obj) => {
11417                assert!(
11418                    obj.get("__tuple__").is_none(),
11419                    "tuple wrapper leaked into output: {:?}",
11420                    value
11421                );
11422                for v in obj.values() {
11423                    assert_no_tuple_wrapper(v);
11424                }
11425            }
11426            JValue::Array(arr) => {
11427                for item in arr.iter() {
11428                    assert_no_tuple_wrapper(item);
11429                }
11430            }
11431            _ => {}
11432        }
11433    }
11434
11435    #[test]
11436    fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
11437        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11438        let ast = crate::parser::parse("items#$i").unwrap();
11439        let mut evaluator = Evaluator::new();
11440        let result = evaluator.evaluate(&ast, &data).unwrap();
11441        assert_no_tuple_wrapper(&result);
11442        assert_eq!(
11443            result,
11444            JValue::array(vec![
11445                JValue::from(1i64),
11446                JValue::from(2i64),
11447                JValue::from(3i64)
11448            ])
11449        );
11450    }
11451
11452    #[test]
11453    fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
11454        // Confirmed by Task 6 to evaluate to the correct @-values but stay
11455        // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
11456        let data = dataset5_for_tuple_tests();
11457        let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
11458        let mut evaluator = Evaluator::new();
11459        let result = evaluator.evaluate(&ast, &data).unwrap();
11460        assert_no_tuple_wrapper(&result);
11461        assert_eq!(
11462            result,
11463            JValue::array(vec![
11464                JValue::string("040657863"),
11465                JValue::string("0406654603"),
11466            ])
11467        );
11468    }
11469
11470    #[test]
11471    fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
11472        // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
11473        let data = dataset5_for_tuple_tests();
11474        let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
11475            .unwrap();
11476        let mut evaluator = Evaluator::new();
11477        let result = evaluator.evaluate(&ast, &data).unwrap();
11478        assert_no_tuple_wrapper(&result);
11479        assert_eq!(
11480            result,
11481            JValue::array(vec![
11482                JValue::string("0406654608"),
11483                JValue::string("0406634348"),
11484            ])
11485        );
11486    }
11487
11488    #[test]
11489    fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
11490        // A tuple-producing expression nested inside a constructed object's field
11491        // value: the top-level result is a plain (non-tuple) Object, so a naive
11492        // "unwrap only if the whole value is a tuple wrapper" check would miss
11493        // this -- must recurse into field values too.
11494        let data = dataset5_for_tuple_tests();
11495        let ast =
11496            crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
11497                .unwrap();
11498        let mut evaluator = Evaluator::new();
11499        let result = evaluator.evaluate(&ast, &data).unwrap();
11500        assert_no_tuple_wrapper(&result);
11501        assert_eq!(
11502            result,
11503            JValue::from(serde_json::json!({
11504                "skus": ["040657863", "0406654603"]
11505            }))
11506        );
11507    }
11508
11509    #[test]
11510    fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
11511        let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11512        let ast = crate::parser::parse("[items#$i]").unwrap();
11513        let mut evaluator = Evaluator::new();
11514        let result = evaluator.evaluate(&ast, &data).unwrap();
11515        assert_no_tuple_wrapper(&result);
11516    }
11517
11518    #[test]
11519    fn test_evaluate_literals() {
11520        let mut evaluator = Evaluator::new();
11521        let data = JValue::Null;
11522
11523        // String literal
11524        let result = evaluator
11525            .evaluate(&AstNode::string("hello"), &data)
11526            .unwrap();
11527        assert_eq!(result, JValue::string("hello"));
11528
11529        // Number literal
11530        let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
11531        assert_eq!(result, JValue::from(42i64));
11532
11533        // Boolean literal
11534        let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
11535        assert_eq!(result, JValue::Bool(true));
11536
11537        // Null literal
11538        let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
11539        assert_eq!(result, JValue::Null);
11540    }
11541
11542    #[test]
11543    fn test_evaluate_variables() {
11544        let mut evaluator = Evaluator::new();
11545        let data = JValue::Null;
11546
11547        // Bind a variable
11548        evaluator
11549            .context
11550            .bind("x".to_string(), JValue::from(100i64));
11551
11552        // Look up the variable
11553        let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
11554        assert_eq!(result, JValue::from(100i64));
11555
11556        // Undefined variable returns null (undefined in JSONata semantics)
11557        let result = evaluator
11558            .evaluate(&AstNode::variable("undefined"), &data)
11559            .unwrap();
11560        assert_eq!(result, JValue::Null);
11561    }
11562
11563    #[test]
11564    fn test_evaluate_path() {
11565        let mut evaluator = Evaluator::new();
11566        let data = JValue::from(serde_json::json!({
11567            "foo": {
11568                "bar": {
11569                    "baz": 42
11570                }
11571            }
11572        }));
11573        // Simple path
11574        let path = AstNode::Path {
11575            steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
11576        };
11577        let result = evaluator.evaluate(&path, &data).unwrap();
11578        assert_eq!(
11579            result,
11580            JValue::from(serde_json::json!({"bar": {"baz": 42}}))
11581        );
11582
11583        // Nested path
11584        let path = AstNode::Path {
11585            steps: vec![
11586                PathStep::new(AstNode::Name("foo".to_string())),
11587                PathStep::new(AstNode::Name("bar".to_string())),
11588                PathStep::new(AstNode::Name("baz".to_string())),
11589            ],
11590        };
11591        let result = evaluator.evaluate(&path, &data).unwrap();
11592        assert_eq!(result, JValue::from(42i64));
11593
11594        // Missing path returns undefined (not null - see issue #32)
11595        let path = AstNode::Path {
11596            steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
11597        };
11598        let result = evaluator.evaluate(&path, &data).unwrap();
11599        assert_eq!(result, JValue::Undefined);
11600    }
11601
11602    #[test]
11603    fn test_arithmetic_operations() {
11604        let mut evaluator = Evaluator::new();
11605        let data = JValue::Null;
11606
11607        // Addition
11608        let expr = AstNode::Binary {
11609            op: BinaryOp::Add,
11610            lhs: Box::new(AstNode::number(10.0)),
11611            rhs: Box::new(AstNode::number(5.0)),
11612        };
11613        let result = evaluator.evaluate(&expr, &data).unwrap();
11614        assert_eq!(result, JValue::Number(15.0));
11615
11616        // Subtraction
11617        let expr = AstNode::Binary {
11618            op: BinaryOp::Subtract,
11619            lhs: Box::new(AstNode::number(10.0)),
11620            rhs: Box::new(AstNode::number(5.0)),
11621        };
11622        let result = evaluator.evaluate(&expr, &data).unwrap();
11623        assert_eq!(result, JValue::Number(5.0));
11624
11625        // Multiplication
11626        let expr = AstNode::Binary {
11627            op: BinaryOp::Multiply,
11628            lhs: Box::new(AstNode::number(10.0)),
11629            rhs: Box::new(AstNode::number(5.0)),
11630        };
11631        let result = evaluator.evaluate(&expr, &data).unwrap();
11632        assert_eq!(result, JValue::Number(50.0));
11633
11634        // Division
11635        let expr = AstNode::Binary {
11636            op: BinaryOp::Divide,
11637            lhs: Box::new(AstNode::number(10.0)),
11638            rhs: Box::new(AstNode::number(5.0)),
11639        };
11640        let result = evaluator.evaluate(&expr, &data).unwrap();
11641        assert_eq!(result, JValue::Number(2.0));
11642
11643        // Modulo
11644        let expr = AstNode::Binary {
11645            op: BinaryOp::Modulo,
11646            lhs: Box::new(AstNode::number(10.0)),
11647            rhs: Box::new(AstNode::number(3.0)),
11648        };
11649        let result = evaluator.evaluate(&expr, &data).unwrap();
11650        assert_eq!(result, JValue::Number(1.0));
11651    }
11652
11653    #[test]
11654    fn test_division_by_zero() {
11655        let mut evaluator = Evaluator::new();
11656        let data = JValue::Null;
11657
11658        let expr = AstNode::Binary {
11659            op: BinaryOp::Divide,
11660            lhs: Box::new(AstNode::number(10.0)),
11661            rhs: Box::new(AstNode::number(0.0)),
11662        };
11663        let result = evaluator.evaluate(&expr, &data);
11664        assert!(result.is_err());
11665    }
11666
11667    #[test]
11668    fn test_comparison_operations() {
11669        let mut evaluator = Evaluator::new();
11670        let data = JValue::Null;
11671
11672        // Equal
11673        let expr = AstNode::Binary {
11674            op: BinaryOp::Equal,
11675            lhs: Box::new(AstNode::number(5.0)),
11676            rhs: Box::new(AstNode::number(5.0)),
11677        };
11678        assert_eq!(
11679            evaluator.evaluate(&expr, &data).unwrap(),
11680            JValue::Bool(true)
11681        );
11682
11683        // Not equal
11684        let expr = AstNode::Binary {
11685            op: BinaryOp::NotEqual,
11686            lhs: Box::new(AstNode::number(5.0)),
11687            rhs: Box::new(AstNode::number(3.0)),
11688        };
11689        assert_eq!(
11690            evaluator.evaluate(&expr, &data).unwrap(),
11691            JValue::Bool(true)
11692        );
11693
11694        // Less than
11695        let expr = AstNode::Binary {
11696            op: BinaryOp::LessThan,
11697            lhs: Box::new(AstNode::number(3.0)),
11698            rhs: Box::new(AstNode::number(5.0)),
11699        };
11700        assert_eq!(
11701            evaluator.evaluate(&expr, &data).unwrap(),
11702            JValue::Bool(true)
11703        );
11704
11705        // Greater than
11706        let expr = AstNode::Binary {
11707            op: BinaryOp::GreaterThan,
11708            lhs: Box::new(AstNode::number(5.0)),
11709            rhs: Box::new(AstNode::number(3.0)),
11710        };
11711        assert_eq!(
11712            evaluator.evaluate(&expr, &data).unwrap(),
11713            JValue::Bool(true)
11714        );
11715    }
11716
11717    #[test]
11718    fn test_logical_operations() {
11719        let mut evaluator = Evaluator::new();
11720        let data = JValue::Null;
11721
11722        // And - both true
11723        let expr = AstNode::Binary {
11724            op: BinaryOp::And,
11725            lhs: Box::new(AstNode::boolean(true)),
11726            rhs: Box::new(AstNode::boolean(true)),
11727        };
11728        assert_eq!(
11729            evaluator.evaluate(&expr, &data).unwrap(),
11730            JValue::Bool(true)
11731        );
11732
11733        // And - first false
11734        let expr = AstNode::Binary {
11735            op: BinaryOp::And,
11736            lhs: Box::new(AstNode::boolean(false)),
11737            rhs: Box::new(AstNode::boolean(true)),
11738        };
11739        assert_eq!(
11740            evaluator.evaluate(&expr, &data).unwrap(),
11741            JValue::Bool(false)
11742        );
11743
11744        // Or - first true
11745        let expr = AstNode::Binary {
11746            op: BinaryOp::Or,
11747            lhs: Box::new(AstNode::boolean(true)),
11748            rhs: Box::new(AstNode::boolean(false)),
11749        };
11750        assert_eq!(
11751            evaluator.evaluate(&expr, &data).unwrap(),
11752            JValue::Bool(true)
11753        );
11754
11755        // Or - both false
11756        let expr = AstNode::Binary {
11757            op: BinaryOp::Or,
11758            lhs: Box::new(AstNode::boolean(false)),
11759            rhs: Box::new(AstNode::boolean(false)),
11760        };
11761        assert_eq!(
11762            evaluator.evaluate(&expr, &data).unwrap(),
11763            JValue::Bool(false)
11764        );
11765    }
11766
11767    #[test]
11768    fn test_string_concatenation() {
11769        let mut evaluator = Evaluator::new();
11770        let data = JValue::Null;
11771
11772        let expr = AstNode::Binary {
11773            op: BinaryOp::Concatenate,
11774            lhs: Box::new(AstNode::string("Hello")),
11775            rhs: Box::new(AstNode::string(" World")),
11776        };
11777        let result = evaluator.evaluate(&expr, &data).unwrap();
11778        assert_eq!(result, JValue::string("Hello World"));
11779    }
11780
11781    #[test]
11782    fn test_range_operator() {
11783        let mut evaluator = Evaluator::new();
11784        let data = JValue::Null;
11785
11786        // Forward range
11787        let expr = AstNode::Binary {
11788            op: BinaryOp::Range,
11789            lhs: Box::new(AstNode::number(1.0)),
11790            rhs: Box::new(AstNode::number(5.0)),
11791        };
11792        let result = evaluator.evaluate(&expr, &data).unwrap();
11793        assert_eq!(
11794            result,
11795            JValue::array(vec![
11796                JValue::Number(1.0),
11797                JValue::Number(2.0),
11798                JValue::Number(3.0),
11799                JValue::Number(4.0),
11800                JValue::Number(5.0)
11801            ])
11802        );
11803
11804        // Backward range (start > end) returns empty array
11805        let expr = AstNode::Binary {
11806            op: BinaryOp::Range,
11807            lhs: Box::new(AstNode::number(5.0)),
11808            rhs: Box::new(AstNode::number(1.0)),
11809        };
11810        let result = evaluator.evaluate(&expr, &data).unwrap();
11811        assert_eq!(result, JValue::array(vec![]));
11812    }
11813
11814    #[test]
11815    fn test_in_operator() {
11816        let mut evaluator = Evaluator::new();
11817        let data = JValue::Null;
11818
11819        // In array
11820        let expr = AstNode::Binary {
11821            op: BinaryOp::In,
11822            lhs: Box::new(AstNode::number(3.0)),
11823            rhs: Box::new(AstNode::Array(vec![
11824                AstNode::number(1.0),
11825                AstNode::number(2.0),
11826                AstNode::number(3.0),
11827            ])),
11828        };
11829        let result = evaluator.evaluate(&expr, &data).unwrap();
11830        assert_eq!(result, JValue::Bool(true));
11831
11832        // Not in array
11833        let expr = AstNode::Binary {
11834            op: BinaryOp::In,
11835            lhs: Box::new(AstNode::number(5.0)),
11836            rhs: Box::new(AstNode::Array(vec![
11837                AstNode::number(1.0),
11838                AstNode::number(2.0),
11839                AstNode::number(3.0),
11840            ])),
11841        };
11842        let result = evaluator.evaluate(&expr, &data).unwrap();
11843        assert_eq!(result, JValue::Bool(false));
11844    }
11845
11846    #[test]
11847    fn test_unary_operations() {
11848        let mut evaluator = Evaluator::new();
11849        let data = JValue::Null;
11850
11851        // Negation
11852        let expr = AstNode::Unary {
11853            op: UnaryOp::Negate,
11854            operand: Box::new(AstNode::number(5.0)),
11855        };
11856        let result = evaluator.evaluate(&expr, &data).unwrap();
11857        assert_eq!(result, JValue::Number(-5.0));
11858
11859        // Not
11860        let expr = AstNode::Unary {
11861            op: UnaryOp::Not,
11862            operand: Box::new(AstNode::boolean(true)),
11863        };
11864        let result = evaluator.evaluate(&expr, &data).unwrap();
11865        assert_eq!(result, JValue::Bool(false));
11866    }
11867
11868    #[test]
11869    fn test_array_construction() {
11870        let mut evaluator = Evaluator::new();
11871        let data = JValue::Null;
11872
11873        let expr = AstNode::Array(vec![
11874            AstNode::number(1.0),
11875            AstNode::number(2.0),
11876            AstNode::number(3.0),
11877        ]);
11878        let result = evaluator.evaluate(&expr, &data).unwrap();
11879        // Whole number literals are preserved as integers
11880        assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
11881    }
11882
11883    #[test]
11884    fn test_object_construction() {
11885        let mut evaluator = Evaluator::new();
11886        let data = JValue::Null;
11887
11888        let expr = AstNode::Object(vec![
11889            (AstNode::string("name"), AstNode::string("Alice")),
11890            (AstNode::string("age"), AstNode::number(30.0)),
11891        ]);
11892        let result = evaluator.evaluate(&expr, &data).unwrap();
11893        // Whole number literals are preserved as integers
11894        let mut expected = IndexMap::new();
11895        expected.insert("name".to_string(), JValue::string("Alice"));
11896        expected.insert("age".to_string(), JValue::Number(30.0));
11897        assert_eq!(result, JValue::object(expected));
11898    }
11899
11900    #[test]
11901    fn test_conditional() {
11902        let mut evaluator = Evaluator::new();
11903        let data = JValue::Null;
11904
11905        // True condition
11906        let expr = AstNode::Conditional {
11907            condition: Box::new(AstNode::boolean(true)),
11908            then_branch: Box::new(AstNode::string("yes")),
11909            else_branch: Some(Box::new(AstNode::string("no"))),
11910        };
11911        let result = evaluator.evaluate(&expr, &data).unwrap();
11912        assert_eq!(result, JValue::string("yes"));
11913
11914        // False condition
11915        let expr = AstNode::Conditional {
11916            condition: Box::new(AstNode::boolean(false)),
11917            then_branch: Box::new(AstNode::string("yes")),
11918            else_branch: Some(Box::new(AstNode::string("no"))),
11919        };
11920        let result = evaluator.evaluate(&expr, &data).unwrap();
11921        assert_eq!(result, JValue::string("no"));
11922
11923        // No else branch returns undefined (not null)
11924        let expr = AstNode::Conditional {
11925            condition: Box::new(AstNode::boolean(false)),
11926            then_branch: Box::new(AstNode::string("yes")),
11927            else_branch: None,
11928        };
11929        let result = evaluator.evaluate(&expr, &data).unwrap();
11930        assert_eq!(result, JValue::Undefined);
11931    }
11932
11933    #[test]
11934    fn test_block_expression() {
11935        let mut evaluator = Evaluator::new();
11936        let data = JValue::Null;
11937
11938        let expr = AstNode::Block(vec![
11939            AstNode::number(1.0),
11940            AstNode::number(2.0),
11941            AstNode::number(3.0),
11942        ]);
11943        let result = evaluator.evaluate(&expr, &data).unwrap();
11944        // Block returns the last expression; whole numbers are preserved as integers
11945        assert_eq!(result, JValue::from(3i64));
11946    }
11947
11948    #[test]
11949    fn test_function_calls() {
11950        let mut evaluator = Evaluator::new();
11951        let data = JValue::Null;
11952
11953        // uppercase function
11954        let expr = AstNode::Function {
11955            name: "uppercase".to_string(),
11956            args: vec![AstNode::string("hello")],
11957            is_builtin: true,
11958        };
11959        let result = evaluator.evaluate(&expr, &data).unwrap();
11960        assert_eq!(result, JValue::string("HELLO"));
11961
11962        // lowercase function
11963        let expr = AstNode::Function {
11964            name: "lowercase".to_string(),
11965            args: vec![AstNode::string("HELLO")],
11966            is_builtin: true,
11967        };
11968        let result = evaluator.evaluate(&expr, &data).unwrap();
11969        assert_eq!(result, JValue::string("hello"));
11970
11971        // length function
11972        let expr = AstNode::Function {
11973            name: "length".to_string(),
11974            args: vec![AstNode::string("hello")],
11975            is_builtin: true,
11976        };
11977        let result = evaluator.evaluate(&expr, &data).unwrap();
11978        assert_eq!(result, JValue::from(5i64));
11979
11980        // sum function
11981        let expr = AstNode::Function {
11982            name: "sum".to_string(),
11983            args: vec![AstNode::Array(vec![
11984                AstNode::number(1.0),
11985                AstNode::number(2.0),
11986                AstNode::number(3.0),
11987            ])],
11988            is_builtin: true,
11989        };
11990        let result = evaluator.evaluate(&expr, &data).unwrap();
11991        assert_eq!(result, JValue::Number(6.0));
11992
11993        // count function
11994        let expr = AstNode::Function {
11995            name: "count".to_string(),
11996            args: vec![AstNode::Array(vec![
11997                AstNode::number(1.0),
11998                AstNode::number(2.0),
11999                AstNode::number(3.0),
12000            ])],
12001            is_builtin: true,
12002        };
12003        let result = evaluator.evaluate(&expr, &data).unwrap();
12004        assert_eq!(result, JValue::from(3i64));
12005    }
12006
12007    #[test]
12008    fn test_complex_nested_data() {
12009        let mut evaluator = Evaluator::new();
12010        let data = JValue::from(serde_json::json!({
12011            "users": [
12012                {"name": "Alice", "age": 30},
12013                {"name": "Bob", "age": 25},
12014                {"name": "Charlie", "age": 35}
12015            ],
12016            "metadata": {
12017                "total": 3,
12018                "version": "1.0"
12019            }
12020        }));
12021        // Access nested field
12022        let path = AstNode::Path {
12023            steps: vec![
12024                PathStep::new(AstNode::Name("metadata".to_string())),
12025                PathStep::new(AstNode::Name("version".to_string())),
12026            ],
12027        };
12028        let result = evaluator.evaluate(&path, &data).unwrap();
12029        assert_eq!(result, JValue::string("1.0"));
12030    }
12031
12032    #[test]
12033    fn test_error_handling() {
12034        let mut evaluator = Evaluator::new();
12035        let data = JValue::Null;
12036
12037        // Type error: adding string and number
12038        let expr = AstNode::Binary {
12039            op: BinaryOp::Add,
12040            lhs: Box::new(AstNode::string("hello")),
12041            rhs: Box::new(AstNode::number(5.0)),
12042        };
12043        let result = evaluator.evaluate(&expr, &data);
12044        assert!(result.is_err());
12045
12046        // Reference error: undefined function
12047        let expr = AstNode::Function {
12048            name: "undefined_function".to_string(),
12049            args: vec![],
12050            is_builtin: false,
12051        };
12052        let result = evaluator.evaluate(&expr, &data);
12053        assert!(result.is_err());
12054    }
12055
12056    #[test]
12057    fn test_truthiness() {
12058        let evaluator = Evaluator::new();
12059
12060        assert!(!evaluator.is_truthy(&JValue::Null));
12061        assert!(!evaluator.is_truthy(&JValue::Bool(false)));
12062        assert!(evaluator.is_truthy(&JValue::Bool(true)));
12063        assert!(!evaluator.is_truthy(&JValue::from(0i64)));
12064        assert!(evaluator.is_truthy(&JValue::from(1i64)));
12065        assert!(!evaluator.is_truthy(&JValue::string("")));
12066        assert!(evaluator.is_truthy(&JValue::string("hello")));
12067        assert!(!evaluator.is_truthy(&JValue::array(vec![])));
12068        assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
12069    }
12070
12071    #[test]
12072    fn test_integration_with_parser() {
12073        use crate::parser::parse;
12074
12075        let mut evaluator = Evaluator::new();
12076        let data = JValue::from(serde_json::json!({
12077            "price": 10,
12078            "quantity": 5
12079        }));
12080        // Test simple path
12081        let ast = parse("price").unwrap();
12082        let result = evaluator.evaluate(&ast, &data).unwrap();
12083        assert_eq!(result, JValue::from(10i64));
12084
12085        // Test arithmetic
12086        let ast = parse("price * quantity").unwrap();
12087        let result = evaluator.evaluate(&ast, &data).unwrap();
12088        // Note: Arithmetic operations produce f64 results in JSON
12089        assert_eq!(result, JValue::Number(50.0));
12090
12091        // Test comparison
12092        let ast = parse("price > 5").unwrap();
12093        let result = evaluator.evaluate(&ast, &data).unwrap();
12094        assert_eq!(result, JValue::Bool(true));
12095    }
12096
12097    #[test]
12098    fn test_evaluate_dollar_function_uppercase() {
12099        use crate::parser::parse;
12100
12101        let mut evaluator = Evaluator::new();
12102        let ast = parse(r#"$uppercase("hello")"#).unwrap();
12103        let empty = JValue::object(IndexMap::new());
12104        let result = evaluator.evaluate(&ast, &empty).unwrap();
12105        assert_eq!(result, JValue::string("HELLO"));
12106    }
12107
12108    #[test]
12109    fn test_evaluate_dollar_function_sum() {
12110        use crate::parser::parse;
12111
12112        let mut evaluator = Evaluator::new();
12113        let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
12114        let empty = JValue::object(IndexMap::new());
12115        let result = evaluator.evaluate(&ast, &empty).unwrap();
12116        assert_eq!(result, JValue::Number(15.0));
12117    }
12118
12119    #[test]
12120    fn test_evaluate_nested_dollar_functions() {
12121        use crate::parser::parse;
12122
12123        let mut evaluator = Evaluator::new();
12124        let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
12125        let empty = JValue::object(IndexMap::new());
12126        let result = evaluator.evaluate(&ast, &empty).unwrap();
12127        // length() returns an integer, not a float
12128        assert_eq!(result, JValue::Number(5.0));
12129    }
12130
12131    #[test]
12132    fn test_array_mapping() {
12133        use crate::parser::parse;
12134
12135        let mut evaluator = Evaluator::new();
12136        let data: JValue = serde_json::from_str(
12137            r#"{
12138            "products": [
12139                {"id": 1, "name": "Laptop", "price": 999.99},
12140                {"id": 2, "name": "Mouse", "price": 29.99},
12141                {"id": 3, "name": "Keyboard", "price": 79.99}
12142            ]
12143        }"#,
12144        )
12145        .map(|v: serde_json::Value| JValue::from(v))
12146        .unwrap();
12147
12148        // Test mapping over array to extract field
12149        let ast = parse("products.name").unwrap();
12150        let result = evaluator.evaluate(&ast, &data).unwrap();
12151        assert_eq!(
12152            result,
12153            JValue::array(vec![
12154                JValue::string("Laptop"),
12155                JValue::string("Mouse"),
12156                JValue::string("Keyboard")
12157            ])
12158        );
12159
12160        // Test mapping over array to extract prices
12161        let ast = parse("products.price").unwrap();
12162        let result = evaluator.evaluate(&ast, &data).unwrap();
12163        assert_eq!(
12164            result,
12165            JValue::array(vec![
12166                JValue::Number(999.99),
12167                JValue::Number(29.99),
12168                JValue::Number(79.99)
12169            ])
12170        );
12171
12172        // Test with $sum function on mapped array
12173        let ast = parse("$sum(products.price)").unwrap();
12174        let result = evaluator.evaluate(&ast, &data).unwrap();
12175        assert_eq!(result, JValue::Number(1109.97));
12176    }
12177
12178    #[test]
12179    fn test_empty_brackets() {
12180        use crate::parser::parse;
12181
12182        let mut evaluator = Evaluator::new();
12183
12184        // Test empty brackets on simple value - should wrap in array
12185        let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
12186        let ast = parse("foo[]").unwrap();
12187        let result = evaluator.evaluate(&ast, &data).unwrap();
12188        assert_eq!(
12189            result,
12190            JValue::array(vec![JValue::string("bar")]),
12191            "Empty brackets should wrap value in array"
12192        );
12193
12194        // Test empty brackets on array - should return array as-is
12195        let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
12196        let ast2 = parse("arr[]").unwrap();
12197        let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
12198        assert_eq!(
12199            result2,
12200            JValue::array(vec![
12201                JValue::Number(1.0),
12202                JValue::Number(2.0),
12203                JValue::Number(3.0)
12204            ]),
12205            "Empty brackets should preserve array"
12206        );
12207    }
12208
12209    // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
12210    // Expected values below are ground-truthed against jsonata-js 2.x.
12211
12212    #[test]
12213    fn test_index_bind_makes_variable_available_in_next_step() {
12214        // `#$o` binds each Order's position; `$o` must resolve in the later step.
12215        let data: JValue = serde_json::json!({
12216            "Account": {
12217                "Order": [
12218                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
12219                    {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
12220                ]
12221            }
12222        })
12223        .into();
12224        let ast =
12225            crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
12226        let mut evaluator = Evaluator::new();
12227        let result = evaluator.evaluate(&ast, &data).unwrap();
12228        assert_eq!(
12229            result,
12230            serde_json::json!([
12231                {"name": "Hat", "idx": 0},
12232                {"name": "Cap", "idx": 1},
12233                {"name": "Sock", "idx": 1}
12234            ])
12235            .into()
12236        );
12237    }
12238
12239    #[test]
12240    fn test_index_bind_with_predicate_stage() {
12241        // Mirrors reference joins/index[13]: index binding, then a predicate on
12242        // the next step, carrying the index binding through.
12243        let data: JValue = serde_json::json!({
12244            "Account": {
12245                "Order": [
12246                    {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
12247                    {"Product": [{"ProductID": 9, "Name": "C"}]}
12248                ]
12249            }
12250        })
12251        .into();
12252        let ast =
12253            crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
12254                .unwrap();
12255        let mut evaluator = Evaluator::new();
12256        let result = evaluator.evaluate(&ast, &data).unwrap();
12257        assert_eq!(
12258            result,
12259            serde_json::json!([
12260                {"n": "B", "idx": 0},
12261                {"n": "C", "idx": 1}
12262            ])
12263            .into()
12264        );
12265    }
12266
12267    #[test]
12268    fn test_focus_bind_makes_variable_available_in_next_step() {
12269        // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
12270        // does NOT advance the context `@`); the variable itself is what carries
12271        // forward. This asserts the real jsonata-js behaviour.
12272        let data: JValue = serde_json::json!({
12273            "Account": {
12274                "Order": [
12275                    {"OrderID": "o1"},
12276                    {"OrderID": "o2"}
12277                ]
12278            }
12279        })
12280        .into();
12281        let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
12282        let mut evaluator = Evaluator::new();
12283        let result = evaluator.evaluate(&ast, &data).unwrap();
12284        assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
12285    }
12286
12287    #[test]
12288    fn test_parent_reference_resolves_to_enclosing_step_value() {
12289        let data: JValue = serde_json::json!({
12290            "Account": {
12291                "Order": [
12292                    {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
12293                ]
12294            }
12295        })
12296        .into();
12297        let ast =
12298            crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
12299                .unwrap();
12300        let mut evaluator = Evaluator::new();
12301        let result = evaluator.evaluate(&ast, &data).unwrap();
12302        assert_eq!(
12303            result,
12304            serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
12305        );
12306    }
12307
12308    // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
12309    // a tuple-carried `$name`/`!label` key straight into the top scope and then
12310    // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
12311    // same-named outer `:=` binding that happened to be live in that scope
12312    // frame. Expected values below are verified against jsonata-js (2.2.1
12313    // reference, `tests/jsonata-js`).
12314
12315    #[test]
12316    fn test_chained_focus_bind_does_not_clobber_outer_variable() {
12317        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12318        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
12319        let mut evaluator = Evaluator::new();
12320        let result = evaluator.evaluate(&ast, &data).unwrap();
12321        assert_eq!(result, serde_json::json!("OUT").into());
12322    }
12323
12324    #[test]
12325    fn test_chained_index_bind_does_not_clobber_outer_variable() {
12326        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12327        let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
12328        let mut evaluator = Evaluator::new();
12329        let result = evaluator.evaluate(&ast, &data).unwrap();
12330        assert_eq!(result, serde_json::json!("OUT").into());
12331    }
12332
12333    #[test]
12334    fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
12335        let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12336        let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
12337        let mut evaluator = Evaluator::new();
12338        let result = evaluator.evaluate(&ast, &data).unwrap();
12339        assert_eq!(result, serde_json::json!("OUT").into());
12340    }
12341
12342    #[test]
12343    fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
12344        let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
12345        let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
12346        let mut evaluator = Evaluator::new();
12347        let result = evaluator.evaluate(&ast, &data).unwrap();
12348        assert_eq!(result, serde_json::json!("OUT").into());
12349    }
12350}