jsonata_core/evaluator.rs
1// Expression evaluator
2// Mirrors jsonata.js from the reference implementation
3
4#![allow(clippy::cloned_ref_to_slice_refs)]
5#![allow(clippy::explicit_counter_loop)]
6#![allow(clippy::too_many_arguments)]
7#![allow(clippy::manual_strip)]
8
9use std::cmp::Ordering;
10use std::collections::{HashMap, HashSet};
11use std::time::Instant;
12
13use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
14use crate::parser;
15use crate::value::JValue;
16use indexmap::IndexMap;
17use std::rc::Rc;
18use thiserror::Error;
19
20/// Specialized sort comparator for `$l.field op $r.field` patterns.
21/// Bypasses the full AST evaluator for simple field-based sort comparisons.
22///
23/// In JSONata `$sort`, the comparator returns true when `$l` should come AFTER `$r`.
24/// `$l.field > $r.field` swaps when left > right, producing ascending order.
25/// `$l.field < $r.field` swaps when left < right, producing descending order.
26struct SpecializedSortComparator {
27 field: String,
28 descending: bool,
29}
30
31/// Pre-extracted sort key for the Schwartzian transform in specialized sorting.
32enum SortKey {
33 Num(f64),
34 Str(Rc<str>),
35 None,
36}
37
38fn compare_sort_keys(a: &SortKey, b: &SortKey, descending: bool) -> Ordering {
39 let ord = match (a, b) {
40 (SortKey::Num(x), SortKey::Num(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
41 (SortKey::Str(x), SortKey::Str(y)) => (**x).cmp(&**y),
42 (SortKey::None, SortKey::None) => Ordering::Equal,
43 (SortKey::None, _) => Ordering::Greater,
44 (_, SortKey::None) => Ordering::Less,
45 // Mixed types: maintain original order
46 _ => Ordering::Equal,
47 };
48 if descending {
49 ord.reverse()
50 } else {
51 ord
52 }
53}
54
55/// Try to extract a specialized sort comparator from a lambda AST node.
56/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
57fn try_specialize_sort_comparator(
58 body: &AstNode,
59 left_param: &str,
60 right_param: &str,
61) -> Option<SpecializedSortComparator> {
62 let AstNode::Binary { op, lhs, rhs } = body else {
63 return None;
64 };
65
66 // Returns true if op means "swap when left > right" (ascending order).
67 let is_ascending = |op: &BinaryOp| -> Option<bool> {
68 match op {
69 BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
70 BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
71 _ => None,
72 }
73 };
74
75 // Extract field name from a `$param.field` path with no stages.
76 let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
77 let AstNode::Path { steps } = node else {
78 return None;
79 };
80 if steps.len() != 2 {
81 return None;
82 }
83 let AstNode::Variable(var) = &steps[0].node else {
84 return None;
85 };
86 if var != param {
87 return None;
88 }
89 let AstNode::Name(field) = &steps[1].node else {
90 return None;
91 };
92 if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() {
93 return None;
94 }
95 Some(field.clone())
96 };
97
98 // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
99 for flipped in [false, true] {
100 let (lhs_param, rhs_param) = if flipped {
101 (right_param, left_param)
102 } else {
103 (left_param, right_param)
104 };
105 if let (Some(lhs_field), Some(rhs_field)) = (
106 extract_var_field(lhs, lhs_param),
107 extract_var_field(rhs, rhs_param),
108 ) {
109 if lhs_field == rhs_field {
110 let descending = match op {
111 // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
112 // Flipped `$r.f - $l.f` → positive when r > l → descending.
113 BinaryOp::Subtract => flipped,
114 // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
115 _ => {
116 let ascending = is_ascending(op)?;
117 if flipped {
118 ascending
119 } else {
120 !ascending
121 }
122 }
123 };
124 return Some(SpecializedSortComparator {
125 field: lhs_field,
126 descending,
127 });
128 }
129 }
130 }
131 None
132}
133
134// ──────────────────────────────────────────────────────────────────────────────
135// CompiledExpr — unified compiled expression framework
136// ──────────────────────────────────────────────────────────────────────────────
137//
138// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
139// can represent arbitrary simple expressions without AST walking. Evaluated in
140// a tight loop with no recursion tracking, no scope management, and no AstNode
141// pattern matching.
142
143/// Shape cache: maps field names to their positional index in an IndexMap.
144/// When all objects in an array share the same key ordering (extremely common
145/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
146/// instead of O(1)-amortized hash lookups.
147type ShapeCache = HashMap<String, usize>;
148
149/// Build a shape cache from the first object in an array.
150/// Returns None if the data is not an object.
151fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
152 match first_element {
153 JValue::Object(obj) => {
154 let mut cache = HashMap::with_capacity(obj.len());
155 for (idx, (key, _)) in obj.iter().enumerate() {
156 cache.insert(key.clone(), idx);
157 }
158 Some(cache)
159 }
160 _ => None,
161 }
162}
163
164/// Comparison operator for compiled expressions.
165#[derive(Debug, Clone, Copy)]
166pub(crate) enum CompiledCmp {
167 Eq,
168 Ne,
169 Lt,
170 Le,
171 Gt,
172 Ge,
173}
174
175/// Arithmetic operator for compiled expressions.
176#[derive(Debug, Clone, Copy)]
177pub(crate) enum CompiledArithOp {
178 Add,
179 Sub,
180 Mul,
181 Div,
182 Mod,
183}
184
185/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
186///
187/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
188/// expression-compile time (once), then `eval_compiled()` evaluates it per
189/// element in O(expression-size) with no heap allocation in the hot path.
190#[derive(Clone, Debug)]
191pub(crate) enum CompiledExpr {
192 // ── Leaves ──────────────────────────────────────────────────────────
193 /// A literal value known at compile time.
194 Literal(JValue),
195 /// Explicit `null` literal from `AstNode::Null`.
196 /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
197 /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
198 ExplicitNull,
199 /// Single-level field lookup on the current object: `obj.get("field")`.
200 FieldLookup(String),
201 /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
202 NestedFieldLookup(String, String),
203 /// Variable lookup from enclosing scope (e.g. `$var`).
204 /// Resolved at eval time via a provided variable map.
205 VariableLookup(String),
206
207 // ── Comparison ──────────────────────────────────────────────────────
208 Compare {
209 op: CompiledCmp,
210 lhs: Box<CompiledExpr>,
211 rhs: Box<CompiledExpr>,
212 },
213
214 // ── Arithmetic ──────────────────────────────────────────────────────
215 Arithmetic {
216 op: CompiledArithOp,
217 lhs: Box<CompiledExpr>,
218 rhs: Box<CompiledExpr>,
219 },
220
221 // ── String ──────────────────────────────────────────────────────────
222 Concat(Box<CompiledExpr>, Box<CompiledExpr>),
223
224 // ── Logical ─────────────────────────────────────────────────────────
225 And(Box<CompiledExpr>, Box<CompiledExpr>),
226 Or(Box<CompiledExpr>, Box<CompiledExpr>),
227 Not(Box<CompiledExpr>),
228 /// Negation of a numeric value.
229 Negate(Box<CompiledExpr>),
230
231 // ── Conditional ─────────────────────────────────────────────────────
232 Conditional {
233 condition: Box<CompiledExpr>,
234 then_expr: Box<CompiledExpr>,
235 else_expr: Option<Box<CompiledExpr>>,
236 },
237
238 // ── Compound ────────────────────────────────────────────────────────
239 /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
240 ObjectConstruct(Vec<(String, CompiledExpr)>),
241 /// Array construction: `[expr1, expr2, ...]`
242 ///
243 /// Each element carries a `bool` flag: `true` means the element originated
244 /// from an explicit `AstNode::Array` constructor and must be kept nested even
245 /// if it evaluates to an array. `false` means the element's array value is
246 /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
247 /// Undefined values are always skipped.
248 ArrayConstruct(Vec<(CompiledExpr, bool)>),
249
250 // ── Phase 2 extensions ──────────────────────────────────────────────
251 /// Named variable lookup from context scope (any `$name` not in lambda params).
252 /// Compiled when a named variable is encountered and no allowed_vars list is
253 /// provided (top-level compilation). At runtime, returns the value from the vars
254 /// map (lambda params or captured env), or Undefined if not present.
255 #[allow(dead_code)]
256 ContextVar(String),
257
258 /// Multi-step field path with optional per-step filters: `a.b[pred].c`
259 /// Applies implicit array-mapping semantics at each step.
260 FieldPath(Vec<CompiledStep>),
261
262 /// Call a pure, side-effect-free builtin with compiled arguments.
263 /// Only builtins in COMPILABLE_BUILTINS are allowed here.
264 BuiltinCall {
265 name: &'static str,
266 args: Vec<CompiledExpr>,
267 },
268
269 /// Sequential block: evaluate all expressions, return last value.
270 Block(Vec<CompiledExpr>),
271
272 /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
273 Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
274
275 // ── Higher-order functions with inline lambdas ───────────────────────
276 /// `$map(array, function($v [, $i]) { body })` — compiled when the second
277 /// argument is an inline lambda literal (not a stored variable).
278 /// `params` holds the lambda parameter names (without `$`), 1 or 2 elements.
279 MapCall {
280 array: Box<CompiledExpr>,
281 params: Vec<String>,
282 body: Box<CompiledExpr>,
283 },
284 /// `$filter(array, function($v [, $i]) { body })` — compiled when the second
285 /// argument is an inline lambda literal.
286 FilterCall {
287 array: Box<CompiledExpr>,
288 params: Vec<String>,
289 body: Box<CompiledExpr>,
290 },
291 /// `$reduce(array, function($acc, $v) { body } [, initial])` — compiled when the
292 /// second argument is an inline lambda literal with exactly 2 parameters.
293 ReduceCall {
294 array: Box<CompiledExpr>,
295 params: Vec<String>,
296 body: Box<CompiledExpr>,
297 initial: Option<Box<CompiledExpr>>,
298 },
299}
300
301/// One step in a compiled `FieldPath`.
302#[derive(Clone, Debug)]
303pub(crate) struct CompiledStep {
304 /// Field name to look up at this step.
305 pub field: String,
306 /// Optional predicate filter compiled from a `Stage::Filter` stage.
307 pub filter: Option<CompiledExpr>,
308}
309
310/// Try to compile an AstNode subtree into a CompiledExpr.
311/// Returns None for anything that requires full AST evaluation (lambda calls,
312/// function calls with side effects, complex paths, etc.).
313pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
314 reject_if_too_large_to_pool(try_compile_expr_inner(node, None)?)
315}
316
317/// Like `try_compile_expr` but additionally allows the specified variable names
318/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
319/// parameters are known and will be provided via the `vars` map at eval time.
320pub(crate) fn try_compile_expr_with_allowed_vars(
321 node: &AstNode,
322 allowed_vars: &[&str],
323) -> Option<CompiledExpr> {
324 reject_if_too_large_to_pool(try_compile_expr_inner(node, Some(allowed_vars))?)
325}
326
327/// Reject (fall back to the tree-walker) a fully-built `CompiledExpr` tree
328/// whose node count could overflow one of `BytecodeCompiler`'s `u16`-indexed
329/// pools (`const_pool` / `string_pool` / `fallback_exprs` / `sub_programs`).
330///
331/// `BytecodeCompiler::compile` is otherwise infallible (`fn compile(&CompiledExpr)
332/// -> BytecodeProgram`, called from `src/compiler.rs`'s own recursive filter-predicate
333/// compilation, `src/vm.rs`, and twice from `src/lib.rs`) - changing its signature to
334/// return `Option`/`Result` so its 4 pool-interning helpers could "abort gracefully"
335/// mid-compilation would ripple to all of those call sites for a bug class that is
336/// already astronomically impractical to trigger (it requires tens of thousands of
337/// distinct string/field-name/literal constants, or that many nested fallback
338/// sub-expressions, inside one compiled expression). Guarding here instead - before
339/// `BytecodeCompiler::compile` is ever invoked - avoids that ripple entirely, matching
340/// the same "compiler declines, tree-walker (which has no such limit) handles it"
341/// architecture used by the `AstNode::Array`/`AstNode::Block` arity guards above.
342fn reject_if_too_large_to_pool(compiled: CompiledExpr) -> Option<CompiledExpr> {
343 if compiled_expr_node_count_exceeds(&compiled, u16::MAX as usize) {
344 None
345 } else {
346 Some(compiled)
347 }
348}
349
350/// Conservative upper bound check: does `expr`'s node count exceed `limit`?
351///
352/// Each of `BytecodeCompiler`'s 4 pools gains at most one entry per countable
353/// unit processed here (fewer, after interning dedup), so the total count
354/// this walk produces is a safe upper bound for every pool's occupancy -
355/// including a pool populated by a *nested*, independently pooled
356/// `BytecodeCompiler` instance (e.g. a `FieldPath` step's filter predicate,
357/// recompiled into its own `BytecodeProgram` in `compiler.rs`'s `FieldPath`
358/// arm), since that nested instance can only ever process a strict subset of
359/// this tree's nodes. Over-counting (e.g. including a filter predicate's
360/// nodes in the outer count even though it is compiled by a separate
361/// `BytecodeCompiler` with its own pools) is safe: it only means falling back
362/// to the tree-walker slightly earlier than strictly necessary.
363///
364/// "Countable unit" is *not* simply "one `CompiledExpr` node": a
365/// `CompiledExpr::FieldPath`'s individual `CompiledStep`s are not
366/// `CompiledExpr` nodes themselves, yet `compiler.rs`'s `FieldPath` arm
367/// interns *every* step's field name into `string_pool` regardless of
368/// whether that step carries a filter predicate. So this walk counts each
369/// `PathStep` as its own unit (in addition to still recursing into any
370/// filter expression the step may have) - a `FieldPath` with N no-filter
371/// steps contributes N units here, matching the N `string_pool` entries it
372/// costs in `compiler.rs`, not zero.
373///
374/// Uses a shared decrementing budget so pathologically large trees bail out
375/// immediately instead of always walking to completion.
376fn compiled_expr_node_count_exceeds(expr: &CompiledExpr, limit: usize) -> bool {
377 fn walk(expr: &CompiledExpr, budget: &mut usize) -> bool {
378 if *budget == 0 {
379 return true;
380 }
381 *budget -= 1;
382 match expr {
383 // ── Leaves: no children ──────────────────────────────────
384 CompiledExpr::Literal(_)
385 | CompiledExpr::ExplicitNull
386 | CompiledExpr::FieldLookup(_)
387 | CompiledExpr::NestedFieldLookup(_, _)
388 | CompiledExpr::VariableLookup(_)
389 | CompiledExpr::ContextVar(_) => false,
390
391 // ── Binary ────────────────────────────────────────────────
392 CompiledExpr::Compare { lhs, rhs, .. }
393 | CompiledExpr::Arithmetic { lhs, rhs, .. }
394 | CompiledExpr::Concat(lhs, rhs)
395 | CompiledExpr::And(lhs, rhs)
396 | CompiledExpr::Or(lhs, rhs)
397 | CompiledExpr::Coalesce(lhs, rhs) => walk(lhs, budget) || walk(rhs, budget),
398
399 // ── Unary ─────────────────────────────────────────────────
400 CompiledExpr::Not(inner) | CompiledExpr::Negate(inner) => walk(inner, budget),
401
402 // ── Conditional ───────────────────────────────────────────
403 CompiledExpr::Conditional {
404 condition,
405 then_expr,
406 else_expr,
407 } => {
408 walk(condition, budget)
409 || walk(then_expr, budget)
410 || else_expr.as_ref().is_some_and(|e| walk(e, budget))
411 }
412
413 // ── Compound ──────────────────────────────────────────────
414 CompiledExpr::ObjectConstruct(pairs) => pairs.iter().any(|(_, v)| walk(v, budget)),
415 CompiledExpr::ArrayConstruct(elems) => elems.iter().any(|(e, _)| walk(e, budget)),
416 CompiledExpr::FieldPath(steps) => {
417 for step in steps.iter() {
418 // Each step interns its field name into `string_pool` in
419 // `compiler.rs` regardless of whether it has a filter -
420 // count the step itself, not just its optional filter.
421 if *budget == 0 {
422 return true;
423 }
424 *budget -= 1;
425 if let Some(filter) = step.filter.as_ref() {
426 if walk(filter, budget) {
427 return true;
428 }
429 }
430 }
431 false
432 }
433 CompiledExpr::BuiltinCall { args, .. } => args.iter().any(|a| walk(a, budget)),
434 CompiledExpr::Block(exprs) => exprs.iter().any(|e| walk(e, budget)),
435
436 // ── Higher-order functions ────────────────────────────────
437 CompiledExpr::MapCall { array, body, .. }
438 | CompiledExpr::FilterCall { array, body, .. } => {
439 walk(array, budget) || walk(body, budget)
440 }
441 CompiledExpr::ReduceCall {
442 array,
443 body,
444 initial,
445 ..
446 } => {
447 walk(array, budget)
448 || walk(body, budget)
449 || initial.as_ref().is_some_and(|i| walk(i, budget))
450 }
451 }
452 }
453 let mut budget = limit;
454 walk(expr, &mut budget)
455}
456
457fn try_compile_expr_inner(node: &AstNode, allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
458 match node {
459 // ── Literals ────────────────────────────────────────────────────
460 AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
461 AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
462 AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
463 AstNode::Null => Some(CompiledExpr::ExplicitNull),
464
465 // ── Field access ────────────────────────────────────────────────
466 AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
467
468 // ── Variable lookup ─────────────────────────────────────────────
469 // $ (empty name) always refers to the current element.
470 // Named variables: in HOF mode (allowed_vars=Some), only compile if the
471 // variable is in the allowed set (lambda params supplied via vars map).
472 // In top-level mode (allowed_vars=None), compile unknown variables as
473 // ContextVar — they return Undefined at runtime when no bindings are passed.
474 AstNode::Variable(var) if var.is_empty() => Some(CompiledExpr::VariableLookup(var.clone())),
475 AstNode::Variable(var) => {
476 if let Some(allowed) = allowed_vars {
477 // HOF mode: only compile if the variable is a known lambda param.
478 if allowed.contains(&var.as_str()) {
479 return Some(CompiledExpr::VariableLookup(var.clone()));
480 }
481 }
482 // Named variables require Context for correct lookup (scope stack, builtins
483 // registry). The compiled fast path passes ctx=None, so fall back to the
484 // tree-walker for all non-empty variable references.
485 None
486 }
487
488 // ── Path expressions ────────────────────────────────────────────
489 AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
490
491 // ── Binary operations ───────────────────────────────────────────
492 AstNode::Binary { op, lhs, rhs } => {
493 let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
494 let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
495 match op {
496 // Comparison
497 BinaryOp::Equal => Some(CompiledExpr::Compare {
498 op: CompiledCmp::Eq,
499 lhs: Box::new(compiled_lhs),
500 rhs: Box::new(compiled_rhs),
501 }),
502 BinaryOp::NotEqual => Some(CompiledExpr::Compare {
503 op: CompiledCmp::Ne,
504 lhs: Box::new(compiled_lhs),
505 rhs: Box::new(compiled_rhs),
506 }),
507 BinaryOp::LessThan => Some(CompiledExpr::Compare {
508 op: CompiledCmp::Lt,
509 lhs: Box::new(compiled_lhs),
510 rhs: Box::new(compiled_rhs),
511 }),
512 BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
513 op: CompiledCmp::Le,
514 lhs: Box::new(compiled_lhs),
515 rhs: Box::new(compiled_rhs),
516 }),
517 BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
518 op: CompiledCmp::Gt,
519 lhs: Box::new(compiled_lhs),
520 rhs: Box::new(compiled_rhs),
521 }),
522 BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
523 op: CompiledCmp::Ge,
524 lhs: Box::new(compiled_lhs),
525 rhs: Box::new(compiled_rhs),
526 }),
527 // Arithmetic
528 BinaryOp::Add => Some(CompiledExpr::Arithmetic {
529 op: CompiledArithOp::Add,
530 lhs: Box::new(compiled_lhs),
531 rhs: Box::new(compiled_rhs),
532 }),
533 BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
534 op: CompiledArithOp::Sub,
535 lhs: Box::new(compiled_lhs),
536 rhs: Box::new(compiled_rhs),
537 }),
538 BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
539 op: CompiledArithOp::Mul,
540 lhs: Box::new(compiled_lhs),
541 rhs: Box::new(compiled_rhs),
542 }),
543 BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
544 op: CompiledArithOp::Div,
545 lhs: Box::new(compiled_lhs),
546 rhs: Box::new(compiled_rhs),
547 }),
548 BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
549 op: CompiledArithOp::Mod,
550 lhs: Box::new(compiled_lhs),
551 rhs: Box::new(compiled_rhs),
552 }),
553 // Logical
554 BinaryOp::And => Some(CompiledExpr::And(
555 Box::new(compiled_lhs),
556 Box::new(compiled_rhs),
557 )),
558 BinaryOp::Or => Some(CompiledExpr::Or(
559 Box::new(compiled_lhs),
560 Box::new(compiled_rhs),
561 )),
562 // String concat
563 BinaryOp::Concatenate => Some(CompiledExpr::Concat(
564 Box::new(compiled_lhs),
565 Box::new(compiled_rhs),
566 )),
567 // Coalesce: return lhs if defined/non-null, else rhs
568 BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
569 Box::new(compiled_lhs),
570 Box::new(compiled_rhs),
571 )),
572 // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
573 _ => None,
574 }
575 }
576
577 // ── Unary operations ────────────────────────────────────────────
578 AstNode::Unary { op, operand } => {
579 let compiled = try_compile_expr_inner(operand, allowed_vars)?;
580 match op {
581 crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
582 crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
583 }
584 }
585
586 // ── Conditional ─────────────────────────────────────────────────
587 AstNode::Conditional {
588 condition,
589 then_branch,
590 else_branch,
591 } => {
592 let cond = try_compile_expr_inner(condition, allowed_vars)?;
593 let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
594 let else_e = match else_branch {
595 Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
596 None => None,
597 };
598 Some(CompiledExpr::Conditional {
599 condition: Box::new(cond),
600 then_expr: Box::new(then_e),
601 else_expr: else_e,
602 })
603 }
604
605 // ── Object construction ─────────────────────────────────────────
606 AstNode::Object(pairs) => {
607 // Instr::MakeObject's operand is a u16 element count - bail out
608 // to the (always-correct, no-limit) tree-walker rather than
609 // silently truncating via CompiledExpr::ObjectConstruct here.
610 if pairs.len() > u16::MAX as usize {
611 return None;
612 }
613 let mut fields = Vec::with_capacity(pairs.len());
614 for (key_node, val_node) in pairs {
615 // Key must be a string literal
616 let key = match key_node {
617 AstNode::String(s) => s.clone(),
618 _ => return None,
619 };
620 let val = try_compile_expr_inner(val_node, allowed_vars)?;
621 fields.push((key, val));
622 }
623 Some(CompiledExpr::ObjectConstruct(fields))
624 }
625
626 // ── Array construction ──────────────────────────────────────────
627 AstNode::Array(elems) => {
628 // Instr::MakeArray's operand is a u16 element count - bail out
629 // to the (always-correct, no-limit) tree-walker rather than
630 // silently truncating via CompiledExpr::ArrayConstruct here.
631 if elems.len() > u16::MAX as usize {
632 return None;
633 }
634 let mut compiled = Vec::with_capacity(elems.len());
635 for elem in elems {
636 // Tag whether the element itself is an array constructor: if so, its
637 // array value must be kept nested rather than flattened (tree-walker parity).
638 let is_nested = matches!(elem, AstNode::Array(_));
639 compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
640 }
641 Some(CompiledExpr::ArrayConstruct(compiled))
642 }
643
644 // ── Block (sequential evaluation) ───────────────────────────────
645 AstNode::Block(exprs) if !exprs.is_empty() => {
646 // Instr::BlockEnd's operand is a u16 element count - bail out to
647 // the (always-correct, no-limit) tree-walker rather than silently
648 // truncating via CompiledExpr::Block here. Same pattern as the
649 // AstNode::Array guard above.
650 if exprs.len() > u16::MAX as usize {
651 return None;
652 }
653 let compiled: Option<Vec<CompiledExpr>> = exprs
654 .iter()
655 .map(|e| try_compile_expr_inner(e, allowed_vars))
656 .collect();
657 compiled.map(CompiledExpr::Block)
658 }
659
660 // ── Pure builtin function calls ──────────────────────────────────
661 AstNode::Function {
662 name,
663 args,
664 is_builtin: true,
665 } => {
666 if is_compilable_builtin(name) {
667 // Arity guard: if the call site passes more args than the builtin accepts,
668 // fall back to the tree-walker so it can raise the correct T0410 error.
669 if let Some(max) = compilable_builtin_max_args(name) {
670 if args.len() > max {
671 return None;
672 }
673 }
674 // Instr::CallBuiltin's arg_count operand is a u8 - this is NOT
675 // already covered by the max-args guard above for variadic
676 // builtins like "merge" (compilable_builtin_max_args returns
677 // None, i.e. unbounded), so a call site with > 255 arguments
678 // would otherwise silently truncate `args.len() as u8`. Bail
679 // out to the tree-walker rather than truncate.
680 if args.len() > u8::MAX as usize {
681 return None;
682 }
683 let compiled_args: Option<Vec<CompiledExpr>> = args
684 .iter()
685 .map(|a| try_compile_expr_inner(a, allowed_vars))
686 .collect();
687 compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
688 name: static_builtin_name(name),
689 args: cargs,
690 })
691 } else {
692 try_compile_hof_expr(name, args, allowed_vars)
693 }
694 }
695
696 // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
697 _ => None,
698 }
699}
700
701/// Extract an inline lambda's params and body from an AST node, returning `None` if the
702/// node is not a simple lambda (i.e. has a signature or is a TCO thunk).
703fn extract_inline_lambda(node: &AstNode) -> Option<(&Vec<String>, &AstNode)> {
704 match node {
705 AstNode::Lambda {
706 params,
707 body,
708 signature: None,
709 thunk: false,
710 } => Some((params, body)),
711 _ => None,
712 }
713}
714
715/// Compile the array argument + lambda body for a HOF call, returning `None` if either
716/// fails to compile. The lambda params are added to the allowed-vars set so the body
717/// can reference them.
718fn compile_hof_array_and_body(
719 array_node: &AstNode,
720 params: &[String],
721 body: &AstNode,
722 allowed_vars: Option<&[&str]>,
723) -> Option<(Box<CompiledExpr>, Box<CompiledExpr>)> {
724 let array = try_compile_expr_inner(array_node, allowed_vars)?;
725 let param_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
726 let compiled_body = try_compile_expr_inner(body, Some(¶m_refs))?;
727 Some((Box::new(array), Box::new(compiled_body)))
728}
729
730/// Try to compile a higher-order function call (`$map`, `$filter`, `$reduce`) when the
731/// callback argument is an inline lambda literal with a compilable body.
732///
733/// Returns `None` when:
734/// - The callback is not an inline lambda (e.g. a stored variable `$f`) — fall back so
735/// the tree-walker can look up the lambda at runtime.
736/// - The lambda has a signature or is a TCO thunk — semantics require the full evaluator.
737/// - The lambda body is not fully compilable — fall back transparently.
738/// - Param count is outside the supported range (see per-function constraints below).
739fn try_compile_hof_expr(
740 name: &str,
741 args: &[AstNode],
742 allowed_vars: Option<&[&str]>,
743) -> Option<CompiledExpr> {
744 match name {
745 "map" | "filter" => {
746 if args.len() != 2 {
747 return None;
748 }
749 let (params, body) = extract_inline_lambda(&args[1])?;
750 if params.is_empty() || params.len() > 2 {
751 return None;
752 }
753 let (array, compiled_body) =
754 compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
755 if name == "map" {
756 Some(CompiledExpr::MapCall {
757 array,
758 params: params.clone(),
759 body: compiled_body,
760 })
761 } else {
762 Some(CompiledExpr::FilterCall {
763 array,
764 params: params.clone(),
765 body: compiled_body,
766 })
767 }
768 }
769 "reduce" => {
770 if args.len() < 2 || args.len() > 3 {
771 return None;
772 }
773 let (params, body) = extract_inline_lambda(&args[1])?;
774 if params.len() != 2 {
775 return None;
776 }
777 let (array, compiled_body) =
778 compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
779 let initial = if args.len() == 3 {
780 Some(Box::new(try_compile_expr_inner(&args[2], allowed_vars)?))
781 } else {
782 None
783 };
784 Some(CompiledExpr::ReduceCall {
785 array,
786 params: params.clone(),
787 body: compiled_body,
788 initial,
789 })
790 }
791 _ => None,
792 }
793}
794
795/// Returns true if the named builtin is pure (no side effects, no context dependency)
796/// and can be safely compiled into a BuiltinCall.
797fn is_compilable_builtin(name: &str) -> bool {
798 matches!(
799 name,
800 "string"
801 | "length"
802 | "substring"
803 | "substringBefore"
804 | "substringAfter"
805 | "uppercase"
806 | "lowercase"
807 | "trim"
808 | "contains"
809 | "split"
810 | "join"
811 | "number"
812 | "floor"
813 | "ceil"
814 | "round"
815 | "abs"
816 | "sqrt"
817 | "sum"
818 | "max"
819 | "min"
820 | "average"
821 | "count"
822 | "boolean"
823 | "not"
824 | "keys"
825 | "append"
826 | "reverse"
827 | "distinct"
828 | "merge"
829 )
830}
831
832/// Maximum number of explicit arguments accepted by each compilable builtin.
833/// Returns `None` for variadic functions with no fixed upper bound.
834/// Used at compile time to fall back to the tree-walker for over-arity calls
835/// (which the tree-walker turns into the correct T0410/T0411 type errors).
836fn compilable_builtin_max_args(name: &str) -> Option<usize> {
837 match name {
838 "string" => Some(2),
839 "length" | "uppercase" | "lowercase" | "trim" => Some(1),
840 "substring" | "split" => Some(3),
841 "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
842 "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
843 "sum" | "max" | "min" | "average" | "count" => Some(1),
844 "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
845 "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
846 _ => None,
847 }
848}
849
850/// Return the `&'static str` for a known compilable builtin name.
851/// SAFETY: only called after `is_compilable_builtin` returns true.
852fn static_builtin_name(name: &str) -> &'static str {
853 match name {
854 "string" => "string",
855 "length" => "length",
856 "substring" => "substring",
857 "substringBefore" => "substringBefore",
858 "substringAfter" => "substringAfter",
859 "uppercase" => "uppercase",
860 "lowercase" => "lowercase",
861 "trim" => "trim",
862 "contains" => "contains",
863 "split" => "split",
864 "join" => "join",
865 "number" => "number",
866 "floor" => "floor",
867 "ceil" => "ceil",
868 "round" => "round",
869 "abs" => "abs",
870 "sqrt" => "sqrt",
871 "sum" => "sum",
872 "max" => "max",
873 "min" => "min",
874 "average" => "average",
875 "count" => "count",
876 "boolean" => "boolean",
877 "not" => "not",
878 "keys" => "keys",
879 "append" => "append",
880 "reverse" => "reverse",
881 "distinct" => "distinct",
882 "merge" => "merge",
883 _ => unreachable!("Not a compilable builtin: {}", name),
884 }
885}
886
887/// Evaluate a compiled expression against a single element.
888///
889/// `data` is the current element (typically an object from an array).
890/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
891///
892/// This is the tight inner loop — no recursion tracking, no scope push/pop,
893/// no AstNode pattern matching.
894#[inline(always)]
895pub(crate) fn eval_compiled(
896 expr: &CompiledExpr,
897 data: &JValue,
898 vars: Option<&HashMap<&str, &JValue>>,
899 options: &EvaluatorOptions,
900 start_time: Option<Instant>,
901) -> Result<JValue, EvaluatorError> {
902 eval_compiled_inner(expr, data, vars, None, None, options, start_time)
903}
904
905/// Like `eval_compiled` but with an optional shape cache for O(1) positional
906/// field access. The shape cache maps field names to their index in the object's
907/// internal Vec, enabling `get_index()` instead of hash lookups.
908#[inline(always)]
909fn eval_compiled_shaped(
910 expr: &CompiledExpr,
911 data: &JValue,
912 vars: Option<&HashMap<&str, &JValue>>,
913 shape: &ShapeCache,
914 options: &EvaluatorOptions,
915 start_time: Option<Instant>,
916) -> Result<JValue, EvaluatorError> {
917 eval_compiled_inner(expr, data, vars, None, Some(shape), options, start_time)
918}
919
920/// Clone the outer variable bindings into a new HashMap with the given capacity hint.
921/// Used by HOF eval arms to create per-iteration variable scopes that merge outer vars
922/// with lambda parameters.
923#[inline]
924fn clone_outer_vars<'a>(
925 vars: Option<&HashMap<&'a str, &'a JValue>>,
926 capacity: usize,
927) -> HashMap<&'a str, &'a JValue> {
928 vars.map(|v| v.iter().map(|(&k, v)| (k, *v)).collect())
929 .unwrap_or_else(|| HashMap::with_capacity(capacity))
930}
931
932fn eval_compiled_inner(
933 expr: &CompiledExpr,
934 data: &JValue,
935 vars: Option<&HashMap<&str, &JValue>>,
936 ctx: Option<&Context>,
937 shape: Option<&ShapeCache>,
938 options: &EvaluatorOptions,
939 start_time: Option<Instant>,
940) -> Result<JValue, EvaluatorError> {
941 // Single, structurally-unbypassable D1012 checkpoint for the entire compiled fast
942 // path. Every route into compiled evaluation -- the VM's EvalFallback, this task's
943 // MapCall/FilterCall/ReduceCall loop bodies, invoke_stored_lambda's compiled fast
944 // path, evaluate_function_call's inline $map/$filter fast-path loops, and any future
945 // caller -- funnels through this one function (both eval_compiled and
946 // eval_compiled_shaped delegate here), so checking once at entry covers all of them
947 // without having to enumerate call sites. Deliberately timeout-only, no depth check:
948 // self-recursive lambdas cannot compile to CompiledExpr, so genuine recursion always
949 // routes through evaluate_internal's own (already guarded) recursion-depth counter.
950 check_loop_timeout(options, start_time)?;
951 match expr {
952 // ── Leaves ──────────────────────────────────────────────────────
953 CompiledExpr::Literal(v) => Ok(v.clone()),
954
955 // ExplicitNull evaluates to Null, but is flagged at compile-time for
956 // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
957 CompiledExpr::ExplicitNull => Ok(JValue::Null),
958
959 CompiledExpr::FieldLookup(field) => match data {
960 JValue::Object(obj) => {
961 // Shape-accelerated: use positional index if available
962 if let Some(shape) = shape {
963 if let Some(&idx) = shape.get(field.as_str()) {
964 return Ok(obj
965 .get_index(idx)
966 .map(|(_, v)| v.clone())
967 .unwrap_or(JValue::Undefined));
968 }
969 }
970 Ok(obj
971 .get(field.as_str())
972 .cloned()
973 .unwrap_or(JValue::Undefined))
974 }
975 _ => Ok(JValue::Undefined),
976 },
977
978 CompiledExpr::NestedFieldLookup(outer, inner) => match data {
979 JValue::Object(obj) => {
980 // Shape-accelerated outer lookup
981 let outer_val = if let Some(shape) = shape {
982 if let Some(&idx) = shape.get(outer.as_str()) {
983 obj.get_index(idx).map(|(_, v)| v)
984 } else {
985 obj.get(outer.as_str())
986 }
987 } else {
988 obj.get(outer.as_str())
989 };
990 Ok(outer_val
991 .and_then(|v| match v {
992 JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
993 _ => None,
994 })
995 .unwrap_or(JValue::Undefined))
996 }
997 _ => Ok(JValue::Undefined),
998 },
999
1000 CompiledExpr::VariableLookup(var) => {
1001 if let Some(vars) = vars {
1002 if let Some(val) = vars.get(var.as_str()) {
1003 return Ok((*val).clone());
1004 }
1005 }
1006 // $ (empty var name) refers to the current data
1007 if var.is_empty() {
1008 return Ok(data.clone());
1009 }
1010 Ok(JValue::Undefined)
1011 }
1012
1013 // ── Comparison ──────────────────────────────────────────────────
1014 CompiledExpr::Compare { op, lhs, rhs } => {
1015 let lhs_explicit_null = is_compiled_explicit_null(lhs);
1016 let rhs_explicit_null = is_compiled_explicit_null(rhs);
1017 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1018 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1019 match op {
1020 CompiledCmp::Eq => Ok(JValue::Bool(crate::functions::array::values_equal(
1021 &left, &right,
1022 ))),
1023 CompiledCmp::Ne => Ok(JValue::Bool(!crate::functions::array::values_equal(
1024 &left, &right,
1025 ))),
1026 CompiledCmp::Lt => compiled_ordered_cmp(
1027 &left,
1028 &right,
1029 lhs_explicit_null,
1030 rhs_explicit_null,
1031 |a, b| a < b,
1032 |a, b| a < b,
1033 ),
1034 CompiledCmp::Le => compiled_ordered_cmp(
1035 &left,
1036 &right,
1037 lhs_explicit_null,
1038 rhs_explicit_null,
1039 |a, b| a <= b,
1040 |a, b| a <= b,
1041 ),
1042 CompiledCmp::Gt => compiled_ordered_cmp(
1043 &left,
1044 &right,
1045 lhs_explicit_null,
1046 rhs_explicit_null,
1047 |a, b| a > b,
1048 |a, b| a > b,
1049 ),
1050 CompiledCmp::Ge => compiled_ordered_cmp(
1051 &left,
1052 &right,
1053 lhs_explicit_null,
1054 rhs_explicit_null,
1055 |a, b| a >= b,
1056 |a, b| a >= b,
1057 ),
1058 }
1059 }
1060
1061 // ── Arithmetic ──────────────────────────────────────────────────
1062 CompiledExpr::Arithmetic { op, lhs, rhs } => {
1063 let lhs_explicit_null = is_compiled_explicit_null(lhs);
1064 let rhs_explicit_null = is_compiled_explicit_null(rhs);
1065 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1066 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1067 compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
1068 }
1069
1070 // ── String concat ───────────────────────────────────────────────
1071 CompiledExpr::Concat(lhs, rhs) => {
1072 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1073 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1074 let ls = compiled_to_concat_string(&left)?;
1075 let rs = compiled_to_concat_string(&right)?;
1076 Ok(JValue::string(format!("{}{}", ls, rs)))
1077 }
1078
1079 // ── Logical ─────────────────────────────────────────────────────
1080 CompiledExpr::And(lhs, rhs) => {
1081 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1082 if !compiled_is_truthy(&left) {
1083 return Ok(JValue::Bool(false));
1084 }
1085 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1086 Ok(JValue::Bool(compiled_is_truthy(&right)))
1087 }
1088 CompiledExpr::Or(lhs, rhs) => {
1089 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1090 if compiled_is_truthy(&left) {
1091 return Ok(JValue::Bool(true));
1092 }
1093 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1094 Ok(JValue::Bool(compiled_is_truthy(&right)))
1095 }
1096 CompiledExpr::Not(inner) => {
1097 let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1098 Ok(JValue::Bool(!compiled_is_truthy(&val)))
1099 }
1100 CompiledExpr::Negate(inner) => {
1101 let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1102 match val {
1103 JValue::Number(n) => Ok(JValue::Number(-n)),
1104 JValue::Null => Ok(JValue::Null),
1105 // Undefined operand propagates through unary minus, matching the tree-walker.
1106 v if v.is_undefined() => Ok(JValue::Undefined),
1107 _ => Err(EvaluatorError::TypeError(
1108 "D1002: Cannot negate non-number value".to_string(),
1109 )),
1110 }
1111 }
1112
1113 // ── Conditional ─────────────────────────────────────────────────
1114 CompiledExpr::Conditional {
1115 condition,
1116 then_expr,
1117 else_expr,
1118 } => {
1119 let cond = eval_compiled_inner(condition, data, vars, ctx, shape, options, start_time)?;
1120 if compiled_is_truthy(&cond) {
1121 eval_compiled_inner(then_expr, data, vars, ctx, shape, options, start_time)
1122 } else if let Some(else_e) = else_expr {
1123 eval_compiled_inner(else_e, data, vars, ctx, shape, options, start_time)
1124 } else {
1125 Ok(JValue::Undefined)
1126 }
1127 }
1128
1129 // ── Object construction ─────────────────────────────────────────
1130 CompiledExpr::ObjectConstruct(fields) => {
1131 let mut result = IndexMap::with_capacity(fields.len());
1132 for (key, expr) in fields {
1133 let value = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1134 if !value.is_undefined() {
1135 result.insert(key.clone(), value);
1136 }
1137 }
1138 Ok(JValue::object(result))
1139 }
1140
1141 // ── Array construction ──────────────────────────────────────────
1142 CompiledExpr::ArrayConstruct(elems) => {
1143 let mut result = Vec::new();
1144 for (elem_expr, is_nested) in elems {
1145 let value =
1146 eval_compiled_inner(elem_expr, data, vars, ctx, shape, options, start_time)?;
1147 // Undefined values are excluded from array constructors (tree-walker parity)
1148 if value.is_undefined() {
1149 continue;
1150 }
1151 if *is_nested {
1152 // Explicit array constructor [...] — keep nested even if it's an array
1153 result.push(value);
1154 } else if let JValue::Array(arr) = value {
1155 // Non-constructor that evaluated to an array — flatten one level
1156 result.extend(arr.iter().cloned());
1157 } else {
1158 result.push(value);
1159 }
1160 }
1161 Ok(JValue::array(result))
1162 }
1163
1164 // ── Phase 2 new variants ─────────────────────────────────────────
1165
1166 // ContextVar: named variable lookup from context scope.
1167 // In top-level mode (ctx=None, no bindings), returns Undefined.
1168 // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
1169 // is only ever populated for top-level calls — always Undefined there.
1170 CompiledExpr::ContextVar(name) => {
1171 // Check vars map first (for lambda params that might shadow context)
1172 if let Some(vars) = vars {
1173 if let Some(val) = vars.get(name.as_str()) {
1174 return Ok((*val).clone());
1175 }
1176 }
1177 // Then check context scope
1178 if let Some(ctx) = ctx {
1179 if let Some(val) = ctx.lookup(name) {
1180 return Ok(val.clone());
1181 }
1182 }
1183 Ok(JValue::Undefined)
1184 }
1185
1186 // FieldPath: multi-step field access with implicit array mapping.
1187 CompiledExpr::FieldPath(steps) => {
1188 compiled_eval_field_path(steps, data, vars, ctx, shape, options, start_time)
1189 }
1190
1191 // BuiltinCall: evaluate all args, dispatch to pure builtin.
1192 CompiledExpr::BuiltinCall { name, args } => {
1193 let mut evaled_args = Vec::with_capacity(args.len());
1194 for arg in args.iter() {
1195 evaled_args.push(eval_compiled_inner(
1196 arg, data, vars, ctx, shape, options, start_time,
1197 )?);
1198 }
1199 call_pure_builtin(name, &evaled_args, data, options)
1200 }
1201
1202 // Block: evaluate each expression in sequence, return the last value.
1203 CompiledExpr::Block(exprs) => {
1204 let mut result = JValue::Undefined;
1205 for expr in exprs.iter() {
1206 result = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1207 }
1208 Ok(result)
1209 }
1210
1211 // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1212 // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1213 CompiledExpr::Coalesce(lhs, rhs) => {
1214 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1215 if left.is_undefined() {
1216 eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)
1217 } else {
1218 Ok(left)
1219 }
1220 }
1221
1222 // ── Higher-order functions ─────────────────────────────────────────────
1223 //
1224 // These variants are emitted by try_compile_hof_expr when the HOF argument
1225 // is an inline lambda literal with a compilable body. Outer vars are merged
1226 // with the lambda params so that nested HOF can access variables from
1227 // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1228 CompiledExpr::MapCall {
1229 array,
1230 params,
1231 body,
1232 } => {
1233 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1234 let single_holder;
1235 let items: &[JValue] = match &arr_val {
1236 JValue::Array(a) => a.as_slice(),
1237 JValue::Undefined => return Ok(JValue::Undefined),
1238 other => {
1239 single_holder = [other.clone()];
1240 &single_holder[..]
1241 }
1242 };
1243 let mut result = Vec::with_capacity(items.len());
1244 let p0 = params.first().map(|s| s.as_str());
1245
1246 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1247 // 2-param lambda (element + index): build per-iteration because idx_val
1248 // is loop-local and cannot outlive the iteration.
1249 for (idx, item) in items.iter().enumerate() {
1250 check_loop_timeout(options, start_time)?;
1251 let idx_val = JValue::Number(idx as f64);
1252 let mut call_vars = clone_outer_vars(vars, 2);
1253 if let Some(p) = p0 {
1254 call_vars.insert(p, item);
1255 }
1256 call_vars.insert(p1, &idx_val);
1257 let mapped = eval_compiled_inner(
1258 body,
1259 data,
1260 Some(&call_vars),
1261 ctx,
1262 shape,
1263 options,
1264 start_time,
1265 )?;
1266 if !mapped.is_undefined() {
1267 result.push(mapped);
1268 }
1269 }
1270 } else if let Some(p0) = p0 {
1271 // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1272 let mut call_vars = clone_outer_vars(vars, 1);
1273 for item in items.iter() {
1274 check_loop_timeout(options, start_time)?;
1275 call_vars.insert(p0, item);
1276 let mapped = eval_compiled_inner(
1277 body,
1278 data,
1279 Some(&call_vars),
1280 ctx,
1281 shape,
1282 options,
1283 start_time,
1284 )?;
1285 if !mapped.is_undefined() {
1286 result.push(mapped);
1287 }
1288 }
1289 }
1290 check_sequence_length(result.len(), options)?;
1291 Ok(if result.is_empty() {
1292 JValue::Undefined
1293 } else {
1294 JValue::array(result)
1295 })
1296 }
1297
1298 CompiledExpr::FilterCall {
1299 array,
1300 params,
1301 body,
1302 } => {
1303 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1304 if arr_val.is_undefined() || arr_val.is_null() {
1305 return Ok(JValue::Undefined);
1306 }
1307 let single_holder;
1308 let (items, was_single) = match &arr_val {
1309 JValue::Array(a) => (a.as_slice(), false),
1310 other => {
1311 single_holder = [other.clone()];
1312 (&single_holder[..], true)
1313 }
1314 };
1315 let mut result = Vec::with_capacity(items.len() / 2);
1316 let p0 = params.first().map(|s| s.as_str());
1317
1318 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1319 for (idx, item) in items.iter().enumerate() {
1320 check_loop_timeout(options, start_time)?;
1321 let idx_val = JValue::Number(idx as f64);
1322 let mut call_vars = clone_outer_vars(vars, 2);
1323 if let Some(p) = p0 {
1324 call_vars.insert(p, item);
1325 }
1326 call_vars.insert(p1, &idx_val);
1327 let pred = eval_compiled_inner(
1328 body,
1329 data,
1330 Some(&call_vars),
1331 ctx,
1332 shape,
1333 options,
1334 start_time,
1335 )?;
1336 if compiled_is_truthy(&pred) {
1337 result.push(item.clone());
1338 }
1339 }
1340 } else if let Some(p0) = p0 {
1341 let mut call_vars = clone_outer_vars(vars, 1);
1342 for item in items.iter() {
1343 check_loop_timeout(options, start_time)?;
1344 call_vars.insert(p0, item);
1345 let pred = eval_compiled_inner(
1346 body,
1347 data,
1348 Some(&call_vars),
1349 ctx,
1350 shape,
1351 options,
1352 start_time,
1353 )?;
1354 if compiled_is_truthy(&pred) {
1355 result.push(item.clone());
1356 }
1357 }
1358 }
1359 if was_single {
1360 Ok(match result.len() {
1361 0 => JValue::Undefined,
1362 1 => {
1363 check_sequence_length(1, options)?;
1364 result.remove(0)
1365 }
1366 _ => {
1367 check_sequence_length(result.len(), options)?;
1368 JValue::array(result)
1369 }
1370 })
1371 } else {
1372 check_sequence_length(result.len(), options)?;
1373 Ok(JValue::array(result))
1374 }
1375 }
1376
1377 CompiledExpr::ReduceCall {
1378 array,
1379 params,
1380 body,
1381 initial,
1382 } => {
1383 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1384 let single_holder;
1385 let items: &[JValue] = match &arr_val {
1386 JValue::Array(a) => a.as_slice(),
1387 JValue::Null => return Ok(JValue::Null),
1388 JValue::Undefined => return Ok(JValue::Undefined),
1389 other => {
1390 single_holder = [other.clone()];
1391 &single_holder[..]
1392 }
1393 };
1394 let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1395 let init_val =
1396 eval_compiled_inner(init_expr, data, vars, ctx, shape, options, start_time)?;
1397 if items.is_empty() {
1398 return Ok(init_val);
1399 }
1400 (0usize, init_val)
1401 } else {
1402 if items.is_empty() {
1403 return Ok(JValue::Null);
1404 }
1405 (1, items[0].clone())
1406 };
1407 let acc_param = params[0].as_str();
1408 let item_param = params[1].as_str();
1409 for item in items[start_idx..].iter() {
1410 check_loop_timeout(options, start_time)?;
1411 // Per-iteration HashMap: &accumulator borrow must be released before we
1412 // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1413 let mut call_vars = clone_outer_vars(vars, 2);
1414 call_vars.insert(acc_param, &accumulator);
1415 call_vars.insert(item_param, item);
1416 let new_acc = eval_compiled_inner(
1417 body,
1418 data,
1419 Some(&call_vars),
1420 ctx,
1421 shape,
1422 options,
1423 start_time,
1424 )?;
1425 drop(call_vars);
1426 accumulator = new_acc;
1427 }
1428 Ok(accumulator)
1429 }
1430 }
1431}
1432
1433/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1434#[inline]
1435pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1436 match value {
1437 JValue::Null | JValue::Undefined => false,
1438 JValue::Bool(b) => *b,
1439 JValue::Number(n) => *n != 0.0,
1440 JValue::String(s) => !s.is_empty(),
1441 JValue::Array(a) => !a.is_empty(),
1442 JValue::Object(o) => !o.is_empty(),
1443 _ => false,
1444 }
1445}
1446
1447/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1448/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1449#[inline]
1450fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1451 matches!(expr, CompiledExpr::ExplicitNull)
1452}
1453
1454/// Ordered comparison for compiled expressions.
1455/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1456#[inline]
1457pub(crate) fn compiled_ordered_cmp(
1458 left: &JValue,
1459 right: &JValue,
1460 left_is_explicit_null: bool,
1461 right_is_explicit_null: bool,
1462 cmp_num: fn(f64, f64) -> bool,
1463 cmp_str: fn(&str, &str) -> bool,
1464) -> Result<JValue, EvaluatorError> {
1465 match (left, right) {
1466 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1467 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1468 // Both null/undefined → undefined
1469 (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1470 (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1471 // Explicit null literal with any non-null type → T2010 error
1472 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1473 "T2010: Type mismatch in comparison".to_string(),
1474 )),
1475 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1476 "T2010: Type mismatch in comparison".to_string(),
1477 )),
1478 // Boolean with undefined → T2010 error
1479 (JValue::Bool(_), JValue::Null | JValue::Undefined)
1480 | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1481 EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1482 ),
1483 // Number or String with implicit undefined (missing field) → undefined result
1484 (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1485 | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1486 Ok(JValue::Null)
1487 }
1488 // Type mismatch (string vs number)
1489 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1490 Err(EvaluatorError::EvaluationError(
1491 "T2009: The expressions on either side of operator must be of the same data type"
1492 .to_string(),
1493 ))
1494 }
1495 _ => Err(EvaluatorError::EvaluationError(
1496 "T2010: Type mismatch in comparison".to_string(),
1497 )),
1498 }
1499}
1500
1501/// Arithmetic for compiled expressions.
1502/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1503#[inline]
1504pub(crate) fn compiled_arithmetic(
1505 op: CompiledArithOp,
1506 left: &JValue,
1507 right: &JValue,
1508 left_is_explicit_null: bool,
1509 right_is_explicit_null: bool,
1510) -> Result<JValue, EvaluatorError> {
1511 let op_sym = match op {
1512 CompiledArithOp::Add => "+",
1513 CompiledArithOp::Sub => "-",
1514 CompiledArithOp::Mul => "*",
1515 CompiledArithOp::Div => "/",
1516 CompiledArithOp::Mod => "%",
1517 };
1518 match (left, right) {
1519 (JValue::Number(a), JValue::Number(b)) => {
1520 let result = match op {
1521 CompiledArithOp::Add => *a + *b,
1522 CompiledArithOp::Sub => *a - *b,
1523 CompiledArithOp::Mul => {
1524 let r = *a * *b;
1525 if r.is_infinite() {
1526 return Err(EvaluatorError::EvaluationError(
1527 "D1001: Number out of range".to_string(),
1528 ));
1529 }
1530 r
1531 }
1532 CompiledArithOp::Div => {
1533 if *b == 0.0 {
1534 return Err(EvaluatorError::EvaluationError(
1535 "Division by zero".to_string(),
1536 ));
1537 }
1538 *a / *b
1539 }
1540 CompiledArithOp::Mod => {
1541 if *b == 0.0 {
1542 return Err(EvaluatorError::EvaluationError(
1543 "Division by zero".to_string(),
1544 ));
1545 }
1546 *a % *b
1547 }
1548 };
1549 Ok(JValue::Number(result))
1550 }
1551 // Explicit null literal → T2002 error (matching tree-walker behavior)
1552 (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1553 Err(EvaluatorError::TypeError(format!(
1554 "T2002: The left side of the {} operator must evaluate to a number",
1555 op_sym
1556 )))
1557 }
1558 (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1559 Err(EvaluatorError::TypeError(format!(
1560 "T2002: The right side of the {} operator must evaluate to a number",
1561 op_sym
1562 )))
1563 }
1564 // Implicit undefined propagation (from missing field) → undefined result
1565 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1566 Ok(JValue::Null)
1567 }
1568 _ => Err(EvaluatorError::TypeError(format!(
1569 "Cannot apply {} to {:?} and {:?}",
1570 op_sym, left, right
1571 ))),
1572 }
1573}
1574
1575/// Convert a value to string for concatenation in compiled expressions.
1576#[inline]
1577pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1578 match value {
1579 JValue::String(s) => Ok(s.to_string()),
1580 JValue::Null | JValue::Undefined => Ok(String::new()),
1581 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1582 match crate::functions::string::string(value, None) {
1583 Ok(JValue::String(s)) => Ok(s.to_string()),
1584 Ok(JValue::Null) => Ok(String::new()),
1585 _ => Err(EvaluatorError::TypeError(
1586 "Cannot concatenate complex types".to_string(),
1587 )),
1588 }
1589 }
1590 _ => Ok(String::new()),
1591 }
1592}
1593
1594/// Equality comparison for the bytecode VM.
1595#[inline]
1596pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> JValue {
1597 JValue::Bool(crate::functions::array::values_equal(lhs, rhs))
1598}
1599
1600/// String concatenation for the bytecode VM.
1601#[inline]
1602pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1603 let l = compiled_to_concat_string(&lhs)?;
1604 let r = compiled_to_concat_string(&rhs)?;
1605 Ok(JValue::string(l + &r))
1606}
1607
1608/// Entry point for the bytecode VM to call pure builtins by name.
1609#[inline]
1610pub(crate) fn call_pure_builtin_by_name(
1611 name: &str,
1612 args: &[JValue],
1613 data: &JValue,
1614 options: &EvaluatorOptions,
1615) -> Result<JValue, EvaluatorError> {
1616 call_pure_builtin(name, args, data, options)
1617}
1618
1619// ──────────────────────────────────────────────────────────────────────────────
1620// Phase 2: path compilation, builtin dispatch, and supporting helpers
1621// ──────────────────────────────────────────────────────────────────────────────
1622
1623/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1624///
1625/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1626/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1627fn try_compile_path(
1628 steps: &[crate::ast::PathStep],
1629 allowed_vars: Option<&[&str]>,
1630) -> Option<CompiledExpr> {
1631 use crate::ast::{AstNode, Stage};
1632
1633 if steps.is_empty() {
1634 return None;
1635 }
1636
1637 // Determine the start of the path:
1638 // `$.field...` → starts from current data (drop the leading `$` step)
1639 // `$var.field` → variable-prefixed paths: not compiled yet, fall back to tree-walker
1640 // `field...` → starts from current data
1641 let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1642 AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1643 AstNode::Variable(_) => return None,
1644 AstNode::Name(_) => steps,
1645 _ => return None,
1646 };
1647
1648 // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1649 // which represent index access in JSONata, not boolean filtering, and the
1650 // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1651 // to stay an array rather than filtering — the tree-walker's
1652 // `evaluate_predicate` special-cases it and the compiled path has no
1653 // equivalent, so bail out rather than silently treating it as `filter(true)`.
1654 let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1655 if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1656 return None;
1657 }
1658 try_compile_expr_inner(node, allowed_vars)
1659 };
1660
1661 // Compile each field step.
1662 // Handles:
1663 // - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1664 // - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1665 // previous step's filter slot, since both encodings have identical runtime semantics.
1666 let mut compiled_steps = Vec::with_capacity(field_steps.len());
1667 for step in field_steps {
1668 // Tuple-stream steps (@ focus / # index / % parent binding) require the
1669 // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1670 // tuple handling). Never compile them to the flat bytecode field path,
1671 // which is unaware of the binding flags and would silently drop them.
1672 if step.focus.is_some()
1673 || step.index_var.is_some()
1674 || step.ancestor_label.is_some()
1675 || step.is_tuple
1676 {
1677 return None;
1678 }
1679 match &step.node {
1680 AstNode::Name(name) => {
1681 let filter = match step.stages.as_slice() {
1682 [] => None,
1683 [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1684 _ => return None,
1685 };
1686 compiled_steps.push(CompiledStep {
1687 field: name.clone(),
1688 filter,
1689 });
1690 }
1691 AstNode::Predicate(filter_node) => {
1692 // Standalone predicate step — fold into the previous Name step's filter slot.
1693 if !step.stages.is_empty() {
1694 return None;
1695 }
1696 let last = compiled_steps.last_mut()?;
1697 if last.filter.is_some() {
1698 return None;
1699 }
1700 last.filter = Some(compile_filter(filter_node)?);
1701 }
1702 _ => return None,
1703 }
1704 }
1705
1706 if compiled_steps.is_empty() {
1707 // Bare `$` with no further field steps — current-data reference
1708 return Some(CompiledExpr::VariableLookup(String::new()));
1709 }
1710
1711 // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1712 // in HOF mode (allowed_vars=Some), where data is always a single Object element
1713 // from an array. In top-level mode (allowed_vars=None), data can itself be an
1714 // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1715 if allowed_vars.is_some() {
1716 if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1717 return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1718 }
1719 if compiled_steps.len() == 2
1720 && compiled_steps[0].filter.is_none()
1721 && compiled_steps[1].filter.is_none()
1722 {
1723 let outer = compiled_steps.remove(0).field;
1724 let inner = compiled_steps.remove(0).field;
1725 return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1726 }
1727 }
1728
1729 Some(CompiledExpr::FieldPath(compiled_steps))
1730}
1731
1732/// Evaluate a compiled `FieldPath` against `data`.
1733///
1734/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1735/// Filters are applied as predicates: truthy elements are kept.
1736///
1737/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1738/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1739/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1740fn compiled_eval_field_path(
1741 steps: &[CompiledStep],
1742 data: &JValue,
1743 vars: Option<&HashMap<&str, &JValue>>,
1744 ctx: Option<&Context>,
1745 shape: Option<&ShapeCache>,
1746 options: &EvaluatorOptions,
1747 start_time: Option<Instant>,
1748) -> Result<JValue, EvaluatorError> {
1749 let mut current = data.clone();
1750 // Track whether the most recent field step mapped over an array (like the tree-walker's
1751 // `did_array_mapping` flag). Filters also count as array operations.
1752 let mut did_array_mapping = false;
1753 for step in steps {
1754 // Determine if this step will do array mapping before we overwrite `current`
1755 let is_array = matches!(current, JValue::Array(_));
1756 // Field access with implicit array mapping
1757 current = compiled_field_step(&step.field, ¤t, options)?;
1758 if is_array {
1759 did_array_mapping = true;
1760 } else {
1761 // Extracting from a single object resets the flag (tree-walker parity)
1762 did_array_mapping = false;
1763 }
1764 // Apply filter if present (filter is an array operation — keep the flag set)
1765 if let Some(filter) = &step.filter {
1766 current =
1767 compiled_apply_filter(filter, ¤t, vars, ctx, shape, options, start_time)?;
1768 // Filter always implies we operated on an array
1769 did_array_mapping = true;
1770 }
1771 }
1772 // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1773 if did_array_mapping {
1774 Ok(match current {
1775 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1776 other => other,
1777 })
1778 } else {
1779 Ok(current)
1780 }
1781}
1782
1783/// Perform a single-field access with implicit array-mapping semantics.
1784///
1785/// - Object: look up `field`, return its value or Undefined
1786/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1787/// (this is a query-result sequence, so D2015 applies — mirrors `evaluate_path`'s
1788/// array-mapping check and `vm.rs`'s `get_field_cached`)
1789/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1790/// - Other: Undefined
1791fn compiled_field_step(
1792 field: &str,
1793 value: &JValue,
1794 options: &EvaluatorOptions,
1795) -> Result<JValue, EvaluatorError> {
1796 match value {
1797 JValue::Object(obj) => {
1798 // Check for tuple: extract from "@" inner object
1799 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1800 if let Some(JValue::Object(inner)) = obj.get("@") {
1801 return Ok(inner.get(field).cloned().unwrap_or(JValue::Undefined));
1802 }
1803 return Ok(JValue::Undefined);
1804 }
1805 Ok(obj.get(field).cloned().unwrap_or(JValue::Undefined))
1806 }
1807 JValue::Array(arr) => {
1808 // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1809 let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1810 if let JValue::Object(obj) = v {
1811 if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1812 return build_shape_cache(v);
1813 }
1814 }
1815 None
1816 });
1817 let mut result = Vec::new();
1818 for item in arr.iter() {
1819 let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1820 // Tuple objects need the recursive path for "@" inner lookup.
1821 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1822 compiled_field_step(field, item, options)?
1823 } else if let Some(&pos) = sh.get(field) {
1824 // Positional access with key verification: guards against heterogeneous
1825 // schemas (objects where the same field is at a different index).
1826 // On a mismatch, fall back to a regular hash lookup.
1827 match obj.get_index(pos) {
1828 Some((k, v)) if k.as_str() == field => v.clone(),
1829 _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1830 }
1831 } else {
1832 // Field not in the first object's schema — fall back to hash lookup
1833 // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1834 obj.get(field).cloned().unwrap_or(JValue::Undefined)
1835 }
1836 } else {
1837 compiled_field_step(field, item, options)?
1838 };
1839 match extracted {
1840 JValue::Undefined => {}
1841 JValue::Array(inner) => result.extend(inner.iter().cloned()),
1842 other => result.push(other),
1843 }
1844 }
1845 check_sequence_length(result.len(), options)?;
1846 Ok(if result.is_empty() {
1847 JValue::Undefined
1848 } else {
1849 JValue::array(result)
1850 })
1851 }
1852 _ => Ok(JValue::Undefined),
1853 }
1854}
1855
1856/// Apply a compiled filter predicate to a value.
1857///
1858/// - Array: return elements for which the predicate is truthy
1859/// - Single value: return it if predicate is truthy, else Undefined
1860/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1861fn compiled_apply_filter(
1862 filter: &CompiledExpr,
1863 value: &JValue,
1864 vars: Option<&HashMap<&str, &JValue>>,
1865 ctx: Option<&Context>,
1866 shape: Option<&ShapeCache>,
1867 options: &EvaluatorOptions,
1868 start_time: Option<Instant>,
1869) -> Result<JValue, EvaluatorError> {
1870 match value {
1871 JValue::Array(arr) => {
1872 let mut result = Vec::new();
1873 // Auto-build shape cache from first element when not provided.
1874 // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1875 let local_shape: Option<ShapeCache> = if shape.is_none() {
1876 arr.first().and_then(build_shape_cache)
1877 } else {
1878 None
1879 };
1880 let effective_shape = shape.or(local_shape.as_ref());
1881 for item in arr.iter() {
1882 check_loop_timeout(options, start_time)?;
1883 let pred = eval_compiled_inner(
1884 filter,
1885 item,
1886 vars,
1887 ctx,
1888 effective_shape,
1889 options,
1890 start_time,
1891 )?;
1892 if compiled_is_truthy(&pred) {
1893 result.push(item.clone());
1894 }
1895 }
1896 if result.is_empty() {
1897 Ok(JValue::Undefined)
1898 } else if result.len() == 1 {
1899 check_sequence_length(1, options)?;
1900 Ok(result.remove(0))
1901 } else {
1902 check_sequence_length(result.len(), options)?;
1903 Ok(JValue::array(result))
1904 }
1905 }
1906 JValue::Undefined => Ok(JValue::Undefined),
1907 _ => {
1908 let pred = eval_compiled_inner(filter, value, vars, ctx, shape, options, start_time)?;
1909 if compiled_is_truthy(&pred) {
1910 Ok(value.clone())
1911 } else {
1912 Ok(JValue::Undefined)
1913 }
1914 }
1915 }
1916}
1917
1918/// Dispatch a pure builtin function call.
1919///
1920/// Replicates the tree-walker's evaluation for the subset of builtins in
1921/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1922/// `data` is the current context value for implicit-argument insertion.
1923fn call_pure_builtin(
1924 name: &str,
1925 args: &[JValue],
1926 data: &JValue,
1927 options: &EvaluatorOptions,
1928) -> Result<JValue, EvaluatorError> {
1929 use crate::functions;
1930
1931 // Apply implicit context insertion matching the tree-walker
1932 let args_storage: Vec<JValue>;
1933 let effective_args: &[JValue] = if args.is_empty() {
1934 match name {
1935 "string" => {
1936 // $string() with a null/undefined context returns undefined, not "null".
1937 // This mirrors the tree-walker's special case at the function-call site.
1938 if data.is_undefined() || data.is_null() {
1939 return Ok(JValue::Undefined);
1940 }
1941 args_storage = vec![data.clone()];
1942 &args_storage
1943 }
1944 "number" | "boolean" | "uppercase" | "lowercase" => {
1945 args_storage = vec![data.clone()];
1946 &args_storage
1947 }
1948 _ => args,
1949 }
1950 } else if args.len() == 1 {
1951 match name {
1952 "substringBefore" | "substringAfter" | "contains" | "split" => {
1953 if matches!(data, JValue::String(_)) {
1954 args_storage = std::iter::once(data.clone())
1955 .chain(args.iter().cloned())
1956 .collect();
1957 &args_storage
1958 } else {
1959 args
1960 }
1961 }
1962 _ => args,
1963 }
1964 } else {
1965 args
1966 };
1967
1968 // Apply undefined propagation: if the first effective argument is Undefined
1969 // and the function propagates undefined, return Undefined immediately.
1970 // This matches the tree-walker's `propagates_undefined` check.
1971 if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
1972 return Ok(JValue::Undefined);
1973 }
1974
1975 match name {
1976 // ── String functions ────────────────────────────────────────────
1977 "string" => {
1978 // Validate the optional prettify argument: must be a boolean.
1979 let prettify = match effective_args.get(1) {
1980 None => None,
1981 Some(JValue::Bool(b)) => Some(*b),
1982 Some(_) => {
1983 return Err(EvaluatorError::TypeError(
1984 "string() prettify parameter must be a boolean".to_string(),
1985 ))
1986 }
1987 };
1988 let arg = effective_args.first().unwrap_or(&JValue::Null);
1989 Ok(functions::string::string(arg, prettify)?)
1990 }
1991 "length" => match effective_args.first() {
1992 Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1993 // Undefined input propagates (caught above by the undefined-propagation guard).
1994 Some(JValue::Undefined) => Ok(JValue::Undefined),
1995 // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1996 // so the test framework accepts it against any expected T-code).
1997 None => Err(EvaluatorError::EvaluationError(
1998 "length() requires exactly 1 argument".to_string(),
1999 )),
2000 // null and any other non-string type → T0410
2001 _ => Err(EvaluatorError::TypeError(
2002 "T0410: Argument 1 of function length does not match function signature"
2003 .to_string(),
2004 )),
2005 },
2006 "uppercase" => match effective_args.first() {
2007 Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
2008 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2009 _ => Err(EvaluatorError::TypeError(
2010 "T0410: Argument 1 of function uppercase does not match function signature"
2011 .to_string(),
2012 )),
2013 },
2014 "lowercase" => match effective_args.first() {
2015 Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
2016 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2017 _ => Err(EvaluatorError::TypeError(
2018 "T0410: Argument 1 of function lowercase does not match function signature"
2019 .to_string(),
2020 )),
2021 },
2022 "trim" => match effective_args.first() {
2023 None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
2024 Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
2025 _ => Err(EvaluatorError::TypeError(
2026 "trim() requires a string argument".to_string(),
2027 )),
2028 },
2029 "substring" => {
2030 if effective_args.len() < 2 {
2031 return Err(EvaluatorError::EvaluationError(
2032 "substring() requires at least 2 arguments".to_string(),
2033 ));
2034 }
2035 match (&effective_args[0], &effective_args[1]) {
2036 (JValue::String(s), JValue::Number(start)) => {
2037 // Optional 3rd arg (length) must be a number if provided.
2038 let length = match effective_args.get(2) {
2039 None => None,
2040 Some(JValue::Number(l)) => Some(*l as i64),
2041 Some(_) => {
2042 return Err(EvaluatorError::TypeError(
2043 "T0410: Argument 3 of function substring does not match function signature"
2044 .to_string(),
2045 ))
2046 }
2047 };
2048 Ok(functions::string::substring(s, *start as i64, length)?)
2049 }
2050 _ => Err(EvaluatorError::TypeError(
2051 "T0410: Argument 1 of function substring does not match function signature"
2052 .to_string(),
2053 )),
2054 }
2055 }
2056 "substringBefore" => {
2057 if effective_args.len() != 2 {
2058 return Err(EvaluatorError::TypeError(
2059 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
2060 ));
2061 }
2062 match (&effective_args[0], &effective_args[1]) {
2063 (JValue::String(s), JValue::String(sep)) => {
2064 Ok(functions::string::substring_before(s, sep)?)
2065 }
2066 // Undefined propagates; null is a type error.
2067 (JValue::Undefined, _) => Ok(JValue::Undefined),
2068 _ => Err(EvaluatorError::TypeError(
2069 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
2070 )),
2071 }
2072 }
2073 "substringAfter" => {
2074 if effective_args.len() != 2 {
2075 return Err(EvaluatorError::TypeError(
2076 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
2077 ));
2078 }
2079 match (&effective_args[0], &effective_args[1]) {
2080 (JValue::String(s), JValue::String(sep)) => {
2081 Ok(functions::string::substring_after(s, sep)?)
2082 }
2083 // Undefined propagates; null is a type error.
2084 (JValue::Undefined, _) => Ok(JValue::Undefined),
2085 _ => Err(EvaluatorError::TypeError(
2086 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
2087 )),
2088 }
2089 }
2090 "contains" => {
2091 if effective_args.len() != 2 {
2092 return Err(EvaluatorError::EvaluationError(
2093 "contains() requires exactly 2 arguments".to_string(),
2094 ));
2095 }
2096 match &effective_args[0] {
2097 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2098 JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
2099 _ => Err(EvaluatorError::TypeError(
2100 "contains() requires a string as the first argument".to_string(),
2101 )),
2102 }
2103 }
2104 "split" => {
2105 if effective_args.len() < 2 {
2106 return Err(EvaluatorError::EvaluationError(
2107 "split() requires at least 2 arguments".to_string(),
2108 ));
2109 }
2110 match &effective_args[0] {
2111 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2112 JValue::String(s) => {
2113 // Validate the optional limit argument — must be a positive number.
2114 let limit = match effective_args.get(2) {
2115 None => None,
2116 Some(JValue::Number(n)) => {
2117 if *n < 0.0 {
2118 return Err(EvaluatorError::EvaluationError(
2119 "D3020: Third argument of split function must be a positive number"
2120 .to_string(),
2121 ));
2122 }
2123 Some(n.floor() as usize)
2124 }
2125 Some(_) => {
2126 return Err(EvaluatorError::TypeError(
2127 "split() limit must be a number".to_string(),
2128 ))
2129 }
2130 };
2131 Ok(functions::string::split(s, &effective_args[1], limit)?)
2132 }
2133 _ => Err(EvaluatorError::TypeError(
2134 "split() requires a string as the first argument".to_string(),
2135 )),
2136 }
2137 }
2138 "join" => {
2139 if effective_args.is_empty() {
2140 return Err(EvaluatorError::TypeError(
2141 "T0410: Argument 1 of function $join does not match function signature"
2142 .to_string(),
2143 ));
2144 }
2145 match &effective_args[0] {
2146 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2147 // Signature: <a<s>s?:s> — first arg must be an array of strings.
2148 JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
2149 Err(EvaluatorError::TypeError(
2150 "T0412: Argument 1 of function $join must be an array of String"
2151 .to_string(),
2152 ))
2153 }
2154 JValue::Array(arr) => {
2155 // All elements must be strings.
2156 for item in arr.iter() {
2157 if !matches!(item, JValue::String(_)) {
2158 return Err(EvaluatorError::TypeError(
2159 "T0412: Argument 1 of function $join must be an array of String"
2160 .to_string(),
2161 ));
2162 }
2163 }
2164 // Validate separator: must be a string if provided.
2165 let separator = match effective_args.get(1) {
2166 None | Some(JValue::Undefined) => None,
2167 Some(JValue::String(s)) => Some(&**s),
2168 Some(_) => {
2169 return Err(EvaluatorError::TypeError(
2170 "T0410: Argument 2 of function $join does not match function signature (expected String)"
2171 .to_string(),
2172 ))
2173 }
2174 };
2175 Ok(functions::string::join(arr, separator)?)
2176 }
2177 JValue::String(s) => Ok(JValue::String(s.clone())),
2178 _ => Err(EvaluatorError::TypeError(
2179 "T0412: Argument 1 of function $join must be an array of String".to_string(),
2180 )),
2181 }
2182 }
2183
2184 // ── Numeric functions ───────────────────────────────────────────
2185 "number" => match effective_args.first() {
2186 Some(v) => Ok(functions::numeric::number(v)?),
2187 None => Err(EvaluatorError::EvaluationError(
2188 "number() requires at least 1 argument".to_string(),
2189 )),
2190 },
2191 "floor" => match effective_args.first() {
2192 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2193 Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
2194 _ => Err(EvaluatorError::TypeError(
2195 "floor() requires a number argument".to_string(),
2196 )),
2197 },
2198 "ceil" => match effective_args.first() {
2199 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2200 Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
2201 _ => Err(EvaluatorError::TypeError(
2202 "ceil() requires a number argument".to_string(),
2203 )),
2204 },
2205 "round" => match effective_args.first() {
2206 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2207 Some(JValue::Number(n)) => {
2208 let precision = effective_args.get(1).and_then(|v| {
2209 if let JValue::Number(p) = v {
2210 Some(*p as i32)
2211 } else {
2212 None
2213 }
2214 });
2215 Ok(functions::numeric::round(*n, precision)?)
2216 }
2217 _ => Err(EvaluatorError::TypeError(
2218 "round() requires a number argument".to_string(),
2219 )),
2220 },
2221 "abs" => match effective_args.first() {
2222 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2223 Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
2224 _ => Err(EvaluatorError::TypeError(
2225 "abs() requires a number argument".to_string(),
2226 )),
2227 },
2228 "sqrt" => match effective_args.first() {
2229 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2230 Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
2231 _ => Err(EvaluatorError::TypeError(
2232 "sqrt() requires a number argument".to_string(),
2233 )),
2234 },
2235
2236 // ── Aggregation functions ───────────────────────────────────────
2237 "sum" => match effective_args.first() {
2238 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2239 None => Err(EvaluatorError::EvaluationError(
2240 "sum() requires exactly 1 argument".to_string(),
2241 )),
2242 Some(JValue::Null) => Ok(JValue::Null),
2243 Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
2244 Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
2245 Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
2246 },
2247 "max" => match effective_args.first() {
2248 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2249 Some(JValue::Null) | None => Ok(JValue::Null),
2250 Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
2251 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2252 _ => Err(EvaluatorError::TypeError(
2253 "max() requires an array or number argument".to_string(),
2254 )),
2255 },
2256 "min" => match effective_args.first() {
2257 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2258 Some(JValue::Null) | None => Ok(JValue::Null),
2259 Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
2260 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2261 _ => Err(EvaluatorError::TypeError(
2262 "min() requires an array or number argument".to_string(),
2263 )),
2264 },
2265 "average" => match effective_args.first() {
2266 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2267 Some(JValue::Null) | None => Ok(JValue::Null),
2268 Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2269 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2270 _ => Err(EvaluatorError::TypeError(
2271 "average() requires an array or number argument".to_string(),
2272 )),
2273 },
2274 "count" => match effective_args.first() {
2275 Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2276 Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2277 Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2278 _ => Ok(JValue::from(1i64)),
2279 },
2280
2281 // ── Boolean / logic ─────────────────────────────────────────────
2282 "boolean" => match effective_args.first() {
2283 Some(v) => Ok(functions::boolean::boolean(v)?),
2284 None => Err(EvaluatorError::EvaluationError(
2285 "boolean() requires 1 argument".to_string(),
2286 )),
2287 },
2288 "not" => match effective_args.first() {
2289 Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2290 None => Err(EvaluatorError::EvaluationError(
2291 "not() requires 1 argument".to_string(),
2292 )),
2293 },
2294
2295 // ── Array functions ─────────────────────────────────────────────
2296 "append" => {
2297 if effective_args.len() != 2 {
2298 return Err(EvaluatorError::EvaluationError(
2299 "append() requires exactly 2 arguments".to_string(),
2300 ));
2301 }
2302 let first = &effective_args[0];
2303 let second = &effective_args[1];
2304 if matches!(second, JValue::Null | JValue::Undefined) {
2305 return Ok(first.clone());
2306 }
2307 if matches!(first, JValue::Null | JValue::Undefined) {
2308 return Ok(second.clone());
2309 }
2310 let arr = match first {
2311 JValue::Array(a) => a.to_vec(),
2312 other => vec![other.clone()],
2313 };
2314 let second_len = match second {
2315 JValue::Array(a) => a.len(),
2316 _ => 1,
2317 };
2318 check_sequence_length(arr.len() + second_len, options)?;
2319 Ok(functions::array::append(&arr, second)?)
2320 }
2321 "reverse" => match effective_args.first() {
2322 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2323 Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2324 _ => Err(EvaluatorError::TypeError(
2325 "reverse() requires an array argument".to_string(),
2326 )),
2327 },
2328 "distinct" => match effective_args.first() {
2329 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2330 Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2331 // Non-array input, and arrays of length <= 1, pass through unchanged
2332 // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2333 Some(other) => Ok(other.clone()),
2334 },
2335
2336 // ── Object functions ────────────────────────────────────────────
2337 "keys" => match effective_args.first() {
2338 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2339 Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2340 Some(JValue::Object(obj)) => {
2341 if obj.is_empty() {
2342 Ok(JValue::Null)
2343 } else {
2344 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2345 check_sequence_length(keys.len(), options)?;
2346 if keys.len() == 1 {
2347 Ok(keys.into_iter().next().unwrap())
2348 } else {
2349 Ok(JValue::array(keys))
2350 }
2351 }
2352 }
2353 Some(JValue::Array(arr)) => {
2354 let mut all_keys: Vec<JValue> = Vec::new();
2355 for item in arr.iter() {
2356 if let JValue::Object(obj) = item {
2357 for key in obj.keys() {
2358 let k = JValue::string(key.clone());
2359 if !all_keys.contains(&k) {
2360 all_keys.push(k);
2361 }
2362 }
2363 }
2364 }
2365 if all_keys.is_empty() {
2366 Ok(JValue::Null)
2367 } else if all_keys.len() == 1 {
2368 Ok(all_keys.into_iter().next().unwrap())
2369 } else {
2370 check_sequence_length(all_keys.len(), options)?;
2371 Ok(JValue::array(all_keys))
2372 }
2373 }
2374 _ => Ok(JValue::Null),
2375 },
2376 "merge" => match effective_args.len() {
2377 0 => Err(EvaluatorError::EvaluationError(
2378 "merge() requires at least 1 argument".to_string(),
2379 )),
2380 1 => match &effective_args[0] {
2381 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2382 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2383 JValue::Object(_) => Ok(effective_args[0].clone()),
2384 _ => Err(EvaluatorError::TypeError(
2385 "merge() requires objects or an array of objects".to_string(),
2386 )),
2387 },
2388 _ => Ok(functions::object::merge(effective_args)?),
2389 },
2390
2391 _ => unreachable!(
2392 "call_pure_builtin called with non-compilable builtin: {}",
2393 name
2394 ),
2395 }
2396}
2397
2398// ──────────────────────────────────────────────────────────────────────────────
2399// End of CompiledExpr framework
2400// ──────────────────────────────────────────────────────────────────────────────
2401
2402/// Functions that propagate undefined (return undefined when given an undefined argument).
2403/// These functions should return null/undefined when their input path doesn't exist,
2404/// rather than throwing a type error.
2405const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2406 "not",
2407 "boolean",
2408 "length",
2409 "number",
2410 "uppercase",
2411 "lowercase",
2412 "substring",
2413 "substringBefore",
2414 "substringAfter",
2415 "string",
2416];
2417
2418/// Check whether a function propagates undefined values
2419fn propagates_undefined(name: &str) -> bool {
2420 UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2421}
2422
2423/// Iterator-based numeric aggregation helpers.
2424/// These avoid cloning values by iterating over references and extracting f64 values directly.
2425mod aggregation {
2426 use super::*;
2427
2428 /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2429 /// Returns Err if any non-numeric value is encountered.
2430 fn for_each_numeric(
2431 arr: &[JValue],
2432 func_name: &str,
2433 mut f: impl FnMut(f64),
2434 ) -> Result<(), EvaluatorError> {
2435 fn recurse(
2436 arr: &[JValue],
2437 func_name: &str,
2438 f: &mut dyn FnMut(f64),
2439 ) -> Result<(), EvaluatorError> {
2440 for value in arr.iter() {
2441 match value {
2442 JValue::Array(inner) => recurse(inner, func_name, f)?,
2443 JValue::Number(n) => {
2444 f(*n);
2445 }
2446 _ => {
2447 return Err(EvaluatorError::TypeError(format!(
2448 "{}() requires all array elements to be numbers",
2449 func_name
2450 )));
2451 }
2452 }
2453 }
2454 Ok(())
2455 }
2456 recurse(arr, func_name, &mut f)
2457 }
2458
2459 /// Count elements in a potentially nested array without cloning.
2460 fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2461 let mut count = 0usize;
2462 for_each_numeric(arr, func_name, |_| count += 1)?;
2463 Ok(count)
2464 }
2465
2466 pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2467 if arr.is_empty() {
2468 return Ok(JValue::from(0i64));
2469 }
2470 let mut total = 0.0f64;
2471 for_each_numeric(arr, "sum", |n| total += n)?;
2472 Ok(JValue::Number(total))
2473 }
2474
2475 pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2476 if arr.is_empty() {
2477 return Ok(JValue::Null);
2478 }
2479 let mut max_val = f64::NEG_INFINITY;
2480 for_each_numeric(arr, "max", |n| {
2481 if n > max_val {
2482 max_val = n;
2483 }
2484 })?;
2485 Ok(JValue::Number(max_val))
2486 }
2487
2488 pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2489 if arr.is_empty() {
2490 return Ok(JValue::Null);
2491 }
2492 let mut min_val = f64::INFINITY;
2493 for_each_numeric(arr, "min", |n| {
2494 if n < min_val {
2495 min_val = n;
2496 }
2497 })?;
2498 Ok(JValue::Number(min_val))
2499 }
2500
2501 pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2502 if arr.is_empty() {
2503 return Ok(JValue::Null);
2504 }
2505 let mut total = 0.0f64;
2506 let count = count_numeric(arr, "average")?;
2507 for_each_numeric(arr, "average", |n| total += n)?;
2508 Ok(JValue::Number(total / count as f64))
2509 }
2510}
2511
2512/// Evaluator errors
2513#[derive(Error, Debug)]
2514pub enum EvaluatorError {
2515 #[error("Type error: {0}")]
2516 TypeError(String),
2517
2518 #[error("Reference error: {0}")]
2519 ReferenceError(String),
2520
2521 #[error("Evaluation error: {0}")]
2522 EvaluationError(String),
2523}
2524
2525impl From<crate::functions::FunctionError> for EvaluatorError {
2526 fn from(e: crate::functions::FunctionError) -> Self {
2527 EvaluatorError::EvaluationError(e.to_string())
2528 }
2529}
2530
2531impl From<crate::datetime::DateTimeError> for EvaluatorError {
2532 fn from(e: crate::datetime::DateTimeError) -> Self {
2533 EvaluatorError::EvaluationError(e.to_string())
2534 }
2535}
2536
2537/// Result of evaluating a lambda body that may be a tail call
2538/// Used for trampoline-based tail call optimization
2539enum LambdaResult {
2540 /// Final value - evaluation is complete
2541 JValue(JValue),
2542 /// Tail call - need to continue with another lambda invocation
2543 TailCall {
2544 /// The lambda to call (boxed to reduce enum size)
2545 lambda: Box<StoredLambda>,
2546 /// Arguments for the call
2547 args: Vec<JValue>,
2548 /// Data context for the call
2549 data: JValue,
2550 },
2551}
2552
2553/// Lambda storage
2554/// Stores the AST of a lambda function along with its parameters, optional signature,
2555/// and captured environment for closures
2556#[derive(Clone, Debug)]
2557pub struct StoredLambda {
2558 pub params: Vec<String>,
2559 pub body: AstNode,
2560 /// Pre-compiled body for use in tight inner loops (HOF fast path).
2561 /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2562 pub(crate) compiled_body: Option<CompiledExpr>,
2563 pub signature: Option<String>,
2564 /// Captured environment bindings for closures
2565 pub captured_env: HashMap<String, JValue>,
2566 /// Captured data context for lexical scoping of bare field names
2567 pub captured_data: Option<JValue>,
2568 /// Whether this lambda's body contains tail calls that can be optimized
2569 pub thunk: bool,
2570}
2571
2572/// A single scope in the scope stack
2573struct Scope {
2574 bindings: HashMap<String, JValue>,
2575 lambdas: HashMap<String, StoredLambda>,
2576}
2577
2578impl Scope {
2579 fn new() -> Self {
2580 Scope {
2581 bindings: HashMap::new(),
2582 lambdas: HashMap::new(),
2583 }
2584 }
2585}
2586
2587/// Evaluation context
2588///
2589/// Holds variable bindings and other state needed during evaluation.
2590/// Uses a scope stack for efficient push/pop instead of clone/restore.
2591pub struct Context {
2592 scope_stack: Vec<Scope>,
2593 parent_data: Option<JValue>,
2594}
2595
2596impl Context {
2597 pub fn new() -> Self {
2598 Context {
2599 scope_stack: vec![Scope::new()],
2600 parent_data: None,
2601 }
2602 }
2603
2604 /// Push a new scope onto the stack
2605 fn push_scope(&mut self) {
2606 self.scope_stack.push(Scope::new());
2607 }
2608
2609 /// Pop the top scope from the stack
2610 fn pop_scope(&mut self) {
2611 if self.scope_stack.len() > 1 {
2612 self.scope_stack.pop();
2613 }
2614 }
2615
2616 /// Pop scope but preserve specified lambdas by moving them to the current top scope
2617 fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2618 if self.scope_stack.len() > 1 {
2619 let popped = self.scope_stack.pop().unwrap();
2620 if !lambda_ids.is_empty() {
2621 let top = self.scope_stack.last_mut().unwrap();
2622 for id in lambda_ids {
2623 if let Some(stored) = popped.lambdas.get(id) {
2624 top.lambdas.insert(id.clone(), stored.clone());
2625 }
2626 }
2627 }
2628 }
2629 }
2630
2631 /// Clear all bindings and lambdas in the top scope without deallocating
2632 fn clear_current_scope(&mut self) {
2633 let top = self.scope_stack.last_mut().unwrap();
2634 top.bindings.clear();
2635 top.lambdas.clear();
2636 }
2637
2638 pub fn bind(&mut self, name: String, value: JValue) {
2639 self.scope_stack
2640 .last_mut()
2641 .unwrap()
2642 .bindings
2643 .insert(name, value);
2644 }
2645
2646 pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2647 self.scope_stack
2648 .last_mut()
2649 .unwrap()
2650 .lambdas
2651 .insert(name, lambda);
2652 }
2653
2654 pub fn unbind(&mut self, name: &str) {
2655 // Remove from top scope only
2656 let top = self.scope_stack.last_mut().unwrap();
2657 top.bindings.remove(name);
2658 top.lambdas.remove(name);
2659 }
2660
2661 pub fn lookup(&self, name: &str) -> Option<&JValue> {
2662 // Walk scope stack from top to bottom
2663 for scope in self.scope_stack.iter().rev() {
2664 if let Some(value) = scope.bindings.get(name) {
2665 return Some(value);
2666 }
2667 }
2668 None
2669 }
2670
2671 pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2672 // Walk scope stack from top to bottom
2673 for scope in self.scope_stack.iter().rev() {
2674 if let Some(lambda) = scope.lambdas.get(name) {
2675 return Some(lambda);
2676 }
2677 }
2678 None
2679 }
2680
2681 pub fn set_parent(&mut self, data: JValue) {
2682 self.parent_data = Some(data);
2683 }
2684
2685 pub fn get_parent(&self) -> Option<&JValue> {
2686 self.parent_data.as_ref()
2687 }
2688
2689 /// Collect all bindings across all scopes (for environment capture).
2690 /// Higher scopes shadow lower scopes.
2691 fn all_bindings(&self) -> HashMap<String, JValue> {
2692 let mut result = HashMap::new();
2693 for scope in &self.scope_stack {
2694 for (k, v) in &scope.bindings {
2695 result.insert(k.clone(), v.clone());
2696 }
2697 }
2698 result
2699 }
2700}
2701
2702impl Default for Context {
2703 fn default() -> Self {
2704 Self::new()
2705 }
2706}
2707
2708/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2709/// ...}`) from a value about to leave the evaluator.
2710///
2711/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2712/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2713/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2714/// path step consumes and re-wraps these as evaluation proceeds, but the
2715/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2716/// because the tuple-producing expression itself is the whole result (a bare
2717/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2718/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2719/// wrapper ends up embedded in a field value or array element rather than at
2720/// the top level. This recurses through both array elements and (non-tuple)
2721/// object field values so both shapes are cleaned up, not just a bare
2722/// top-level tuple array.
2723/// Merge a group of tuple wrappers into a single tuple, appending each key's
2724/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2725/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2726/// a key present in one tuple stays a scalar; a key present in several becomes an
2727/// array of the collected values (used by group-by value evaluation so a group
2728/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2729/// N collected focus values).
2730fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2731 fn append(acc: Option<JValue>, v: JValue) -> JValue {
2732 match acc {
2733 None => v,
2734 Some(a) => {
2735 let mut out: Vec<JValue> = match a {
2736 JValue::Array(arr) => arr.iter().cloned().collect(),
2737 other => vec![other],
2738 };
2739 match v {
2740 JValue::Array(arr) => out.extend(arr.iter().cloned()),
2741 other => out.push(other),
2742 }
2743 JValue::array(out)
2744 }
2745 }
2746 }
2747 let mut result: IndexMap<String, JValue> = IndexMap::new();
2748 for tuple in group {
2749 if let JValue::Object(obj) = tuple {
2750 for (k, v) in obj.iter() {
2751 if k == "__tuple__" {
2752 result.insert(k.clone(), v.clone());
2753 continue;
2754 }
2755 let merged = append(result.shift_remove(k), v.clone());
2756 result.insert(k.clone(), merged);
2757 }
2758 }
2759 }
2760 result
2761}
2762
2763fn unwrap_tuple_output(value: JValue) -> JValue {
2764 match value {
2765 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2766 .get("@")
2767 .cloned()
2768 .map(unwrap_tuple_output)
2769 .unwrap_or(JValue::Undefined),
2770 JValue::Object(obj) => {
2771 let mut new_map = IndexMap::with_capacity(obj.len());
2772 for (k, v) in obj.iter() {
2773 new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2774 }
2775 JValue::object(new_map)
2776 }
2777 JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2778 other => other,
2779 }
2780}
2781
2782/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2783/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2784/// (if anything) was bound under that name beforehand. `restore` puts the
2785/// prior value back -- or removes the binding entirely if there wasn't
2786/// one -- rather than unconditionally unbinding, so a tuple key that
2787/// happens to share a name with a live outer `:=` binding in the same
2788/// scope frame doesn't get permanently deleted once the tuple-row
2789/// evaluation finishes.
2790struct TupleKeyBindings {
2791 saved: Vec<(String, Option<JValue>)>,
2792}
2793
2794impl TupleKeyBindings {
2795 /// True if `name` was one of the keys this guard bound (used by callers
2796 /// that need to know whether a given tuple key is already in scope
2797 /// before binding it a second time under a different role, e.g.
2798 /// `create_tuple_stream`'s ancestor-label handling).
2799 fn contains(&self, name: &str) -> bool {
2800 self.saved.iter().any(|(n, _)| n == name)
2801 }
2802
2803 fn restore(self, evaluator: &mut Evaluator) {
2804 for (name, prior) in self.saved {
2805 match prior {
2806 Some(value) => evaluator.context.bind(name, value),
2807 None => evaluator.context.unbind(&name),
2808 }
2809 }
2810 }
2811}
2812
2813/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
2814/// evaluator options. All fields default to `None` = unlimited (current behavior).
2815#[derive(Default, Clone, Debug)]
2816pub struct EvaluatorOptions {
2817 /// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
2818 pub timeout_ms: Option<u64>,
2819 /// Maximum AST-recursion stack depth. Exceeding it raises D1011 if this is the
2820 /// tighter of this value and the hardcoded native-stack safety ceiling (302);
2821 /// otherwise the hardcoded ceiling still raises U1001 (see GitHub issue #34).
2822 pub max_stack_depth: Option<usize>,
2823 /// Maximum length of a query-result sequence (map/filter/wildcard/descendants/
2824 /// keys/lookup/append/spread/each/range/path-mapping). Exceeding it raises D2015.
2825 /// Does NOT currently apply to literal array construction (`MakeArray`/
2826 /// `ArrayConstruct`) — NOTE this is a deliberate, temporary divergence from
2827 /// upstream, not a match: jsonata-js DOES cap flat/non-nested array literals
2828 /// (via `fn.append`'s `createSequence` hook in `evaluateUnary`'s `[` case).
2829 /// Deferred until the separate `MakeArray(u16)` truncation bug is fixed (see
2830 /// the design spec's "Sequence length → D2015" section).
2831 pub max_sequence_length: Option<usize>,
2832}
2833
2834/// Checks a constructed query-result sequence's length against the configured
2835/// `max_sequence_length` guardrail. Call this at sites that build a query-result
2836/// sequence (map/filter/wildcard/descendants/keys/lookup/append/spread/each/range/
2837/// path-mapping). NOT currently called at literal array construction (`[1,2,3]`) —
2838/// unlike upstream jsonata-js, which caps flat/non-nested literals too via
2839/// `fn.append`'s `createSequence()` hook. See `EvaluatorOptions::max_sequence_length`
2840/// doc comment above for why this is a deliberate, temporary gap.
2841pub(crate) fn check_sequence_length(
2842 len: usize,
2843 options: &EvaluatorOptions,
2844) -> Result<(), EvaluatorError> {
2845 if let Some(max) = options.max_sequence_length {
2846 if len > max {
2847 return Err(EvaluatorError::EvaluationError(format!(
2848 "D2015: The maximum sequence length of {} was exceeded.",
2849 max
2850 )));
2851 }
2852 }
2853 Ok(())
2854}
2855
2856/// Per-iteration D1012 check for loop-based compiled/VM constructs (map/filter/
2857/// reduce element loops, FilterByBytecode) that don't pass through
2858/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed.
2859#[inline]
2860pub(crate) fn check_loop_timeout(
2861 options: &EvaluatorOptions,
2862 start_time: Option<Instant>,
2863) -> Result<(), EvaluatorError> {
2864 if let Some(timeout_ms) = options.timeout_ms {
2865 if let Some(start) = start_time {
2866 if start.elapsed().as_millis() as u64 > timeout_ms {
2867 return Err(EvaluatorError::EvaluationError(format!(
2868 "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
2869 timeout_ms
2870 )));
2871 }
2872 }
2873 }
2874 Ok(())
2875}
2876
2877/// Evaluator for JSONata expressions
2878pub struct Evaluator {
2879 context: Context,
2880 recursion_depth: usize,
2881 max_recursion_depth: usize,
2882 /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
2883 /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
2884 /// using the AST node's pointer address (as before) collided whenever the same
2885 /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
2886 /// or other repeated recursion), aliasing unrelated closures that shared an id.
2887 next_lambda_id: u64,
2888 /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
2889 /// wrapper during this top-level `evaluate()` call. Reset at the start of
2890 /// `evaluate()` and checked at the end to decide whether the (recursive,
2891 /// O(result size)) tuple-unwrap pass is needed before returning to the
2892 /// caller — keeps the vast majority of evaluations, which never touch
2893 /// `%`/`@`/`#`, at zero added cost.
2894 tuple_stream_created: bool,
2895 /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
2896 /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
2897 /// by the two consumers that read those carried bindings directly from the
2898 /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
2899 /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
2900 /// evaluating its input path (key/value expressions read `$focus` off the
2901 /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
2902 /// of projecting each tuple's `@`.
2903 keep_tuple_stream: bool,
2904 options: EvaluatorOptions,
2905 /// Set in `evaluate()` (only when `options.timeout_ms` is configured) and
2906 /// checked in `evaluate_internal`'s per-node checkpoint for D1012.
2907 start_time: Option<Instant>,
2908}
2909
2910impl Evaluator {
2911 pub fn new() -> Self {
2912 Evaluator {
2913 context: Context::new(),
2914 recursion_depth: 0,
2915 // Limit recursion depth to prevent stack overflow
2916 // True TCO would allow deeper recursion but requires parser-level thunk marking
2917 max_recursion_depth: 302,
2918 next_lambda_id: 0,
2919 tuple_stream_created: false,
2920 keep_tuple_stream: false,
2921 options: EvaluatorOptions::default(),
2922 start_time: None,
2923 }
2924 }
2925
2926 pub fn with_context(context: Context) -> Self {
2927 Evaluator {
2928 context,
2929 recursion_depth: 0,
2930 max_recursion_depth: 302,
2931 next_lambda_id: 0,
2932 tuple_stream_created: false,
2933 keep_tuple_stream: false,
2934 options: EvaluatorOptions::default(),
2935 start_time: None,
2936 }
2937 }
2938
2939 /// Construct an `Evaluator` with guardrail options. `Evaluator::new()`/
2940 /// `with_context()` remain unchanged (unlimited options) for existing callers.
2941 pub fn with_options(context: Context, options: EvaluatorOptions) -> Self {
2942 Evaluator {
2943 context,
2944 recursion_depth: 0,
2945 max_recursion_depth: 302,
2946 next_lambda_id: 0,
2947 tuple_stream_created: false,
2948 keep_tuple_stream: false,
2949 options,
2950 start_time: None,
2951 }
2952 }
2953
2954 /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
2955 fn fresh_lambda_id(&mut self) -> u64 {
2956 let id = self.next_lambda_id;
2957 self.next_lambda_id += 1;
2958 id
2959 }
2960
2961 /// Invoke a stored lambda with its captured environment and data.
2962 /// This is the standard way to call a StoredLambda, handling the
2963 /// captured_env and captured_data extraction boilerplate.
2964 fn invoke_stored_lambda(
2965 &mut self,
2966 stored: &StoredLambda,
2967 args: &[JValue],
2968 data: &JValue,
2969 ) -> Result<JValue, EvaluatorError> {
2970 // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2971 // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2972 // and no captured lambda/builtin values (those require Context for runtime lookup).
2973 if let Some(ref ce) = stored.compiled_body {
2974 if stored.signature.is_none()
2975 && !stored.thunk
2976 && !stored
2977 .captured_env
2978 .values()
2979 .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2980 {
2981 let call_data = stored.captured_data.as_ref().unwrap_or(data);
2982 let vars: HashMap<&str, &JValue> = stored
2983 .params
2984 .iter()
2985 .zip(args.iter())
2986 .map(|(p, v)| (p.as_str(), v))
2987 .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2988 .collect();
2989 return eval_compiled(ce, call_data, Some(&vars), &self.options, self.start_time);
2990 }
2991 }
2992
2993 let captured_env = if stored.captured_env.is_empty() {
2994 None
2995 } else {
2996 Some(&stored.captured_env)
2997 };
2998 let captured_data = stored.captured_data.as_ref();
2999 self.invoke_lambda_with_env(
3000 &stored.params,
3001 &stored.body,
3002 stored.signature.as_ref(),
3003 args,
3004 data,
3005 captured_env,
3006 captured_data,
3007 stored.thunk,
3008 )
3009 }
3010
3011 /// Look up a StoredLambda from a JValue that may be a lambda marker.
3012 /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
3013 /// with a valid lambda_id that references a stored lambda.
3014 fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
3015 if let JValue::Lambda { lambda_id, .. } = value {
3016 return self.context.lookup_lambda(lambda_id).cloned();
3017 }
3018 None
3019 }
3020
3021 /// Get the number of parameters a callback function expects by inspecting its AST.
3022 /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
3023 /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
3024 fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
3025 match func_node {
3026 AstNode::Lambda { params, .. } => params.len(),
3027 AstNode::Variable(var_name) => {
3028 // Check if this variable holds a stored lambda
3029 if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
3030 return stored_lambda.params.len();
3031 }
3032 // Also check if it's a lambda value in bindings (e.g., from partial application)
3033 if let Some(value) = self.context.lookup(var_name) {
3034 if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
3035 return stored_lambda.params.len();
3036 }
3037 }
3038 // Unknown, return max to be safe
3039 usize::MAX
3040 }
3041 AstNode::Function { .. } => {
3042 // For function references, we can't easily determine param count
3043 // Return max to be safe
3044 usize::MAX
3045 }
3046 _ => usize::MAX,
3047 }
3048 }
3049
3050 /// Specialized sort using pre-extracted keys (Schwartzian transform).
3051 /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
3052 /// avoiding O(N log N) hash lookups during comparisons.
3053 fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
3054 if arr.len() <= 1 {
3055 return;
3056 }
3057
3058 // Phase 1: Extract sort keys -- one IndexMap lookup per element
3059 let keys: Vec<SortKey> = arr
3060 .iter()
3061 .map(|item| match item {
3062 JValue::Object(obj) => match obj.get(&spec.field) {
3063 Some(JValue::Number(n)) => SortKey::Num(*n),
3064 Some(JValue::String(s)) => SortKey::Str(s.clone()),
3065 _ => SortKey::None,
3066 },
3067 _ => SortKey::None,
3068 })
3069 .collect();
3070
3071 // Phase 2: Build index permutation sorted by pre-extracted keys
3072 let mut perm: Vec<usize> = (0..arr.len()).collect();
3073 perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
3074
3075 // Phase 3: Apply permutation in-place via cycle-following
3076 let mut placed = vec![false; arr.len()];
3077 for i in 0..arr.len() {
3078 if placed[i] || perm[i] == i {
3079 continue;
3080 }
3081 let mut j = i;
3082 loop {
3083 let target = perm[j];
3084 placed[j] = true;
3085 if target == i {
3086 break;
3087 }
3088 arr.swap(j, target);
3089 j = target;
3090 }
3091 }
3092 }
3093
3094 /// Merge sort implementation using a comparator function.
3095 /// This replaces the O(n²) bubble sort for better performance on large arrays.
3096 /// The comparator returns true if the first element should come AFTER the second.
3097 fn merge_sort_with_comparator(
3098 &mut self,
3099 arr: &mut [JValue],
3100 comparator: &AstNode,
3101 data: &JValue,
3102 ) -> Result<(), EvaluatorError> {
3103 if arr.len() <= 1 {
3104 return Ok(());
3105 }
3106
3107 // Try specialized fast path for simple field comparisons like
3108 // function($l, $r) { $l.price > $r.price }
3109 if let AstNode::Lambda { params, body, .. } = comparator {
3110 if params.len() >= 2 {
3111 if let Some(spec) = try_specialize_sort_comparator(body, ¶ms[0], ¶ms[1]) {
3112 Self::merge_sort_specialized(arr, &spec);
3113 return Ok(());
3114 }
3115 }
3116 }
3117
3118 let mid = arr.len() / 2;
3119
3120 // Sort left half
3121 self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
3122
3123 // Sort right half
3124 self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
3125
3126 // Merge the sorted halves
3127 let mut temp = Vec::with_capacity(arr.len());
3128 let (left, right) = arr.split_at(mid);
3129
3130 let mut i = 0;
3131 let mut j = 0;
3132
3133 // For lambda comparators, use a reusable scope to avoid
3134 // push_scope/pop_scope per comparison (~n log n total comparisons)
3135 if let AstNode::Lambda { params, body, .. } = comparator {
3136 if params.len() >= 2 {
3137 // Pre-clone param names once outside the loop
3138 let param0 = params[0].clone();
3139 let param1 = params[1].clone();
3140 self.context.push_scope();
3141 while i < left.len() && j < right.len() {
3142 // Reuse scope: clear and rebind instead of push/pop
3143 self.context.clear_current_scope();
3144 self.context.bind(param0.clone(), left[i].clone());
3145 self.context.bind(param1.clone(), right[j].clone());
3146
3147 let cmp_result = self.evaluate_internal(body, data)?;
3148
3149 if self.is_truthy(&cmp_result) {
3150 temp.push(right[j].clone());
3151 j += 1;
3152 } else {
3153 temp.push(left[i].clone());
3154 i += 1;
3155 }
3156 }
3157 self.context.pop_scope();
3158 } else {
3159 // Unexpected param count - fall back to generic path
3160 while i < left.len() && j < right.len() {
3161 let cmp_result = self.apply_function(
3162 comparator,
3163 &[left[i].clone(), right[j].clone()],
3164 data,
3165 )?;
3166 if self.is_truthy(&cmp_result) {
3167 temp.push(right[j].clone());
3168 j += 1;
3169 } else {
3170 temp.push(left[i].clone());
3171 i += 1;
3172 }
3173 }
3174 }
3175 } else {
3176 // Non-lambda comparator: use generic apply_function path
3177 while i < left.len() && j < right.len() {
3178 let cmp_result =
3179 self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
3180 if self.is_truthy(&cmp_result) {
3181 temp.push(right[j].clone());
3182 j += 1;
3183 } else {
3184 temp.push(left[i].clone());
3185 i += 1;
3186 }
3187 }
3188 }
3189
3190 // Copy remaining elements
3191 temp.extend_from_slice(&left[i..]);
3192 temp.extend_from_slice(&right[j..]);
3193
3194 // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
3195 for (i, val) in temp.into_iter().enumerate() {
3196 arr[i] = val;
3197 }
3198
3199 Ok(())
3200 }
3201
3202 /// Evaluate an AST node against data
3203 ///
3204 /// This is the main entry point for evaluation. It sets up the parent context
3205 /// to be the root data if not already set.
3206 ///
3207 /// Also the single choke point for stripping any lingering tuple-stream
3208 /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
3209 /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
3210 /// tuple-stream representation (see `create_tuple_stream`), and without this
3211 /// a bare (or object/array-nested) tuple-producing expression would leak
3212 /// that internal representation into user-visible output instead of the
3213 /// plain value.
3214 pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
3215 // Set parent context to root data if not already set
3216 if self.context.get_parent().is_none() {
3217 self.context.set_parent(data.clone());
3218 }
3219
3220 if self.options.timeout_ms.is_some() {
3221 self.start_time = Some(Instant::now());
3222 }
3223
3224 self.tuple_stream_created = false;
3225 let result = self.evaluate_internal(node, data)?;
3226 Ok(if self.tuple_stream_created {
3227 unwrap_tuple_output(result)
3228 } else {
3229 result
3230 })
3231 }
3232
3233 /// Fast evaluation for leaf nodes that don't need recursion tracking.
3234 /// Returns Some for literals, simple field access on objects, and simple variable lookups.
3235 /// Returns None for anything requiring the full evaluator.
3236 #[inline(always)]
3237 fn evaluate_leaf(
3238 &mut self,
3239 node: &AstNode,
3240 data: &JValue,
3241 ) -> Option<Result<JValue, EvaluatorError>> {
3242 match node {
3243 AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
3244 AstNode::Number(n) => {
3245 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3246 Some(Ok(JValue::from(*n as i64)))
3247 } else {
3248 Some(Ok(JValue::Number(*n)))
3249 }
3250 }
3251 AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
3252 AstNode::Null => Some(Ok(JValue::Null)),
3253 AstNode::Undefined => Some(Ok(JValue::Undefined)),
3254 AstNode::Name(field_name) => match data {
3255 // Array mapping and other cases need full evaluator
3256 JValue::Object(obj) => Some(Ok(obj
3257 .get(field_name)
3258 .cloned()
3259 .unwrap_or(JValue::Undefined))),
3260 _ => None,
3261 },
3262 AstNode::Variable(name) if !name.is_empty() => {
3263 // Simple variable lookup — only fast-path when no tuple data
3264 if let JValue::Object(obj) = data {
3265 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3266 return None; // Tuple data needs full evaluator
3267 }
3268 }
3269 // May be a lambda/builtin — needs full evaluator if None
3270 self.context.lookup(name).map(|value| Ok(value.clone()))
3271 }
3272 _ => None,
3273 }
3274 }
3275
3276 /// Internal evaluation method
3277 fn evaluate_internal(
3278 &mut self,
3279 node: &AstNode,
3280 data: &JValue,
3281 ) -> Result<JValue, EvaluatorError> {
3282 // Fast path for leaf nodes — skip recursion tracking overhead
3283 if let Some(result) = self.evaluate_leaf(node, data) {
3284 return result;
3285 }
3286
3287 // Check recursion depth to prevent stack overflow. `effective_limit` is
3288 // whichever is tighter: the user's `max_stack_depth` guardrail or the
3289 // hardcoded native-stack-safety ceiling (`max_recursion_depth`, always
3290 // 302, GitHub issue #34). The hardcoded ceiling is an always-on backstop
3291 // regardless of user options — only a user limit BELOW it can produce
3292 // D1011; hitting the hardcoded ceiling itself (no option set, or an
3293 // option set at/above 302) still produces U1001.
3294 self.recursion_depth += 1;
3295 let effective_limit = match self.options.max_stack_depth {
3296 Some(limit) => limit.min(self.max_recursion_depth),
3297 None => self.max_recursion_depth,
3298 };
3299 if self.recursion_depth > effective_limit {
3300 self.recursion_depth -= 1;
3301 return Err(EvaluatorError::EvaluationError(
3302 if effective_limit < self.max_recursion_depth {
3303 "D1011: Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive".to_string()
3304 } else {
3305 format!(
3306 "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
3307 effective_limit
3308 )
3309 },
3310 ));
3311 }
3312
3313 // Check evaluation timeout (D1012). `start_time` is only set (in
3314 // `evaluate()`) when `options.timeout_ms` is configured, so this is a
3315 // single `is_none()` branch of overhead when no timeout is set.
3316 if let Some(timeout_ms) = self.options.timeout_ms {
3317 if let Some(start) = self.start_time {
3318 if start.elapsed().as_millis() as u64 > timeout_ms {
3319 self.recursion_depth -= 1;
3320 return Err(EvaluatorError::EvaluationError(format!(
3321 "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3322 timeout_ms
3323 )));
3324 }
3325 }
3326 }
3327
3328 // The soft depth counter above is calibrated against a comfortably
3329 // large native stack. Hosts with a much smaller default thread stack
3330 // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
3331 // stack well before this counter trips, crashing the process instead
3332 // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
3333 // transparently swaps in a bigger stack segment when headroom is
3334 // low, so this stays a no-op cost on the common shallow path.
3335 const RED_ZONE: usize = 128 * 1024;
3336 const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
3337 let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
3338 self.evaluate_internal_impl(node, data)
3339 });
3340
3341 self.recursion_depth -= 1;
3342 result
3343 }
3344
3345 /// Internal evaluation implementation (separated to allow depth tracking)
3346 fn evaluate_internal_impl(
3347 &mut self,
3348 node: &AstNode,
3349 data: &JValue,
3350 ) -> Result<JValue, EvaluatorError> {
3351 match node {
3352 AstNode::String(s) => Ok(JValue::string(s.clone())),
3353
3354 // Name nodes represent field access on the current data
3355 AstNode::Name(field_name) => {
3356 match data {
3357 JValue::Object(obj) => {
3358 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3359 }
3360 JValue::Array(arr) => {
3361 // Map over array
3362 let mut result = Vec::new();
3363 for item in arr.iter() {
3364 if let JValue::Object(obj) = item {
3365 if let Some(val) = obj.get(field_name) {
3366 result.push(val.clone());
3367 }
3368 }
3369 }
3370 if result.is_empty() {
3371 Ok(JValue::Undefined)
3372 } else if result.len() == 1 {
3373 Ok(result.into_iter().next().unwrap())
3374 } else {
3375 Ok(JValue::array(result))
3376 }
3377 }
3378 _ => Ok(JValue::Undefined),
3379 }
3380 }
3381
3382 AstNode::Number(n) => {
3383 // Preserve integer-ness: if the number is a whole number, create an integer JValue
3384 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3385 // It's a whole number that can be represented as i64
3386 Ok(JValue::from(*n as i64))
3387 } else {
3388 Ok(JValue::Number(*n))
3389 }
3390 }
3391 AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3392 AstNode::Null => Ok(JValue::Null),
3393 AstNode::Undefined => Ok(JValue::Undefined),
3394 AstNode::Placeholder => {
3395 // Placeholders should only appear as function arguments
3396 // If we reach here, it's an error
3397 Err(EvaluatorError::EvaluationError(
3398 "Placeholder '?' can only be used as a function argument".to_string(),
3399 ))
3400 }
3401 AstNode::Regex { pattern, flags } => {
3402 // Return a regex object as a special JSON value
3403 // This will be recognized by functions like $split, $match, $replace
3404 Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3405 }
3406
3407 AstNode::Variable(name) => {
3408 // Special case: $ alone (empty name) refers to current context
3409 // First check if $ is bound in the context (for closures that captured $)
3410 // Otherwise, use the data parameter
3411 if name.is_empty() {
3412 if let Some(value) = self.context.lookup("$") {
3413 return Ok(value.clone());
3414 }
3415 // If data is a tuple, return the @ value
3416 if let JValue::Object(obj) = data {
3417 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3418 if let Some(inner) = obj.get("@") {
3419 return Ok(inner.clone());
3420 }
3421 }
3422 }
3423 return Ok(data.clone());
3424 }
3425
3426 // Check variable bindings FIRST
3427 // This allows function parameters to shadow outer lambdas with the same name
3428 // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3429 if let Some(value) = self.context.lookup(name) {
3430 return Ok(value.clone());
3431 }
3432
3433 // Check tuple bindings in data (for index binding operator #$var)
3434 // When iterating over a tuple stream, $var can reference the bound index
3435 if let JValue::Object(obj) = data {
3436 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3437 // Check for the variable in tuple bindings (stored as "$name")
3438 let binding_key = format!("${}", name);
3439 if let Some(binding_value) = obj.get(&binding_key) {
3440 return Ok(binding_value.clone());
3441 }
3442 }
3443 }
3444
3445 // Then check if this is a stored lambda (user-defined functions)
3446 if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3447 // Return a lambda representation that can be passed to higher-order functions
3448 // Include _lambda_id pointing to the stored lambda so it can be found
3449 // when captured in closures
3450 let lambda_repr = JValue::lambda(
3451 name.as_str(),
3452 stored_lambda.params.clone(),
3453 Some(name.to_string()),
3454 stored_lambda.signature.clone(),
3455 );
3456 return Ok(lambda_repr);
3457 }
3458
3459 // Check if this is a built-in function reference (only if not shadowed)
3460 if self.is_builtin_function(name) {
3461 // Return a marker for built-in functions
3462 // This allows built-in functions to be passed to higher-order functions
3463 let builtin_repr = JValue::builtin(name.as_str());
3464 return Ok(builtin_repr);
3465 }
3466
3467 // Undefined variable - return null (undefined in JSONata semantics)
3468 // This allows expressions like `$not(undefined_var)` to return undefined
3469 // and comparisons like `3 > $undefined` to return undefined
3470 Ok(JValue::Null)
3471 }
3472
3473 AstNode::ParentVariable(name) => {
3474 // Special case: $$ alone (empty name) refers to parent/root context
3475 if name.is_empty() {
3476 return self.context.get_parent().cloned().ok_or_else(|| {
3477 EvaluatorError::ReferenceError("Parent context not available".to_string())
3478 });
3479 }
3480
3481 // For $$name, we need to evaluate name against parent context
3482 // This is similar to $.name but using parent data
3483 let parent_data = self.context.get_parent().ok_or_else(|| {
3484 EvaluatorError::ReferenceError("Parent context not available".to_string())
3485 })?;
3486
3487 // Access field on parent context
3488 match parent_data {
3489 JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3490 _ => Ok(JValue::Null),
3491 }
3492 }
3493
3494 AstNode::Path { steps } => self.evaluate_path(steps, data),
3495
3496 AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3497
3498 AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3499
3500 // Array constructor - JSONata semantics:
3501 AstNode::Array(elements) => {
3502 // - If element is itself an array constructor [...], keep it nested
3503 // - Otherwise, if element evaluates to an array, flatten it
3504 // - Undefined values are excluded
3505 let mut result = Vec::with_capacity(elements.len());
3506 for element in elements {
3507 // Check if this element is itself an explicit array constructor
3508 let is_array_constructor = matches!(element, AstNode::Array(_));
3509
3510 let value = self.evaluate_internal(element, data)?;
3511
3512 // Skip undefined values in array constructors
3513 // Note: explicit null is preserved, only undefined (no value) is filtered
3514 if value.is_undefined() {
3515 continue;
3516 }
3517
3518 if is_array_constructor {
3519 // Explicit array constructor - keep nested
3520 result.push(value);
3521 } else if let JValue::Array(arr) = value {
3522 // Non-array-constructor that evaluated to array - flatten it
3523 result.extend(arr.iter().cloned());
3524 } else {
3525 // Non-array value - add as-is
3526 result.push(value);
3527 }
3528 }
3529 Ok(JValue::array(result))
3530 }
3531
3532 AstNode::Object(pairs) => {
3533 let mut result = IndexMap::with_capacity(pairs.len());
3534
3535 // Check if all keys are string literals — can skip D1009 HashMap
3536 let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3537
3538 if all_literal_keys {
3539 // Fast path: literal keys, no need for D1009 tracking
3540 for (key_node, value_node) in pairs.iter() {
3541 let key = match key_node {
3542 AstNode::String(s) => s,
3543 _ => unreachable!(),
3544 };
3545 let value = self.evaluate_internal(value_node, data)?;
3546 if value.is_undefined() {
3547 continue;
3548 }
3549 result.insert(key.clone(), value);
3550 }
3551 } else {
3552 let mut key_sources: HashMap<String, usize> = HashMap::new();
3553 for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3554 let key = match self.evaluate_internal(key_node, data)? {
3555 JValue::String(s) => s,
3556 JValue::Null => continue,
3557 other => {
3558 if other.is_undefined() {
3559 continue;
3560 }
3561 return Err(EvaluatorError::TypeError(format!(
3562 "Object key must be a string, got: {:?}",
3563 other
3564 )));
3565 }
3566 };
3567
3568 if let Some(&existing_idx) = key_sources.get(&*key) {
3569 if existing_idx != pair_index {
3570 return Err(EvaluatorError::EvaluationError(format!(
3571 "D1009: Multiple key expressions evaluate to same key: {}",
3572 key
3573 )));
3574 }
3575 }
3576 key_sources.insert(key.to_string(), pair_index);
3577
3578 let value = self.evaluate_internal(value_node, data)?;
3579 if value.is_undefined() {
3580 continue;
3581 }
3582 result.insert(key.to_string(), value);
3583 }
3584 }
3585 Ok(JValue::object(result))
3586 }
3587
3588 // Object transform: group items by key, then evaluate value once per group
3589 AstNode::ObjectTransform { input, pattern } => {
3590 // Evaluate the input expression. Keep tuple wrappers alive so the
3591 // group-by key/value expressions can read the carried `$focus`
3592 // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3593 let saved_keep = self.keep_tuple_stream;
3594 self.keep_tuple_stream = true;
3595 let input_value = self.evaluate_internal(input, data);
3596 self.keep_tuple_stream = saved_keep;
3597 let input_value = input_value?;
3598
3599 // If input is undefined, return undefined (not empty object)
3600 if input_value.is_undefined() {
3601 return Ok(JValue::Undefined);
3602 }
3603
3604 // Handle array input - process each item
3605 let items: Vec<JValue> = match input_value {
3606 JValue::Array(ref arr) => (**arr).clone(),
3607 JValue::Null => return Ok(JValue::Null),
3608 other => vec![other],
3609 };
3610
3611 // If array is empty, add undefined to enable literal JSON object generation
3612 let items = if items.is_empty() {
3613 vec![JValue::Undefined]
3614 } else {
3615 items
3616 };
3617
3618 // Grouping over a tuple stream ("reduce" mode, mirroring
3619 // jsonata-js evaluateGroupExpression): each item is a
3620 // `{@, $var, !label, __tuple__}` wrapper. The key/value
3621 // expressions are evaluated against the tuple's `@` value with the
3622 // carried focus/index/ancestor keys bound into scope (so
3623 // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3624 // `$e` AND resolves the relative `Phone` against the Contact `@`),
3625 // and grouped tuples are reduced (per-key values appended) before
3626 // the value expression sees them.
3627 let reduce = items.first().is_some_and(|it| {
3628 matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3629 });
3630
3631 // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3632 // returns the saved prior values so they can be restored.
3633 let bind_tuple = |ev: &mut Self,
3634 tuple: &IndexMap<String, JValue>|
3635 -> Vec<(String, Option<JValue>)> {
3636 let mut saved = Vec::new();
3637 for (k, v) in tuple.iter() {
3638 let name = if let Some(n) = k.strip_prefix('$') {
3639 if n.is_empty() {
3640 continue;
3641 } else {
3642 n.to_string()
3643 }
3644 } else if k.starts_with('!') {
3645 k.clone()
3646 } else {
3647 continue;
3648 };
3649 saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3650 ev.context.bind(name, v.clone());
3651 }
3652 saved
3653 };
3654 let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3655 for (name, old) in saved.into_iter().rev() {
3656 match old {
3657 Some(v) => ev.context.bind(name, v),
3658 None => ev.context.unbind(&name),
3659 }
3660 }
3661 };
3662
3663 // Phase 1: Group items by key expression
3664 // groups maps key -> (grouped_data, expr_index)
3665 // When multiple items have same key, their data is appended together
3666 let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3667
3668 // Save the current $ binding to restore later
3669 let saved_dollar = self.context.lookup("$").cloned();
3670
3671 for item in &items {
3672 // In reduce mode evaluate the key against `@` with tuple keys
3673 // bound; otherwise against the item itself.
3674 let (key_data, tuple_saved) = match (reduce, item) {
3675 (true, JValue::Object(o)) => {
3676 let saved = bind_tuple(self, o);
3677 (
3678 o.get("@").cloned().unwrap_or(JValue::Undefined),
3679 Some(saved),
3680 )
3681 }
3682 _ => (item.clone(), None),
3683 };
3684 self.context.bind("$".to_string(), key_data.clone());
3685
3686 for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3687 // Evaluate key with current item as context
3688 let key = match self.evaluate_internal(key_node, &key_data)? {
3689 JValue::String(s) => s,
3690 JValue::Null => continue, // Skip null keys
3691 other => {
3692 // Skip undefined keys
3693 if other.is_undefined() {
3694 continue;
3695 }
3696 if let Some(saved) = tuple_saved {
3697 restore(self, saved);
3698 }
3699 return Err(EvaluatorError::TypeError(format!(
3700 "T1003: Object key must be a string, got: {:?}",
3701 other
3702 )));
3703 }
3704 };
3705
3706 // Group items by key
3707 if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3708 // Key already exists - check if from same expression index
3709 if *existing_idx != pair_index {
3710 if let Some(saved) = tuple_saved {
3711 restore(self, saved);
3712 }
3713 // D1009: multiple key expressions evaluate to same key
3714 return Err(EvaluatorError::EvaluationError(format!(
3715 "D1009: Multiple key expressions evaluate to same key: {}",
3716 key
3717 )));
3718 }
3719 // Append item to the group
3720 existing_data.push(item.clone());
3721 } else {
3722 // New key - create new group
3723 groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3724 }
3725 }
3726
3727 if let Some(saved) = tuple_saved {
3728 restore(self, saved);
3729 }
3730 }
3731
3732 // Phase 2: Evaluate value expression for each group
3733 let mut result = IndexMap::new();
3734
3735 for (key, (grouped_data, expr_index)) in groups {
3736 // Get the value expression for this group
3737 let (_key_node, value_node) = &pattern[expr_index];
3738
3739 if reduce {
3740 // Reduce the grouped tuples into one (per-key values
3741 // appended), mirroring jsonata-js reduceTupleStream, then
3742 // evaluate the value against the merged `@` with the merged
3743 // focus/index/ancestor keys bound.
3744 let merged = reduce_tuple_stream(&grouped_data);
3745 let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3746 let mut tuple_no_at = merged.clone();
3747 tuple_no_at.shift_remove("@");
3748 let saved = bind_tuple(self, &tuple_no_at);
3749 self.context.bind("$".to_string(), context.clone());
3750 let value = self.evaluate_internal(value_node, &context);
3751 restore(self, saved);
3752 let value = value?;
3753 if !value.is_undefined() {
3754 result.insert(key, value);
3755 }
3756 continue;
3757 }
3758
3759 // Determine the context for value evaluation:
3760 // - If single item, use that item directly
3761 // - If multiple items, use the array of items
3762 let context = if grouped_data.len() == 1 {
3763 grouped_data.into_iter().next().unwrap()
3764 } else {
3765 JValue::array(grouped_data)
3766 };
3767
3768 // Bind $ to the context for value evaluation
3769 self.context.bind("$".to_string(), context.clone());
3770
3771 // Evaluate value expression with grouped context
3772 let value = self.evaluate_internal(value_node, &context)?;
3773
3774 // Skip undefined values
3775 if !value.is_undefined() {
3776 result.insert(key, value);
3777 }
3778 }
3779
3780 // Restore the previous $ binding
3781 if let Some(saved) = saved_dollar {
3782 self.context.bind("$".to_string(), saved);
3783 } else {
3784 self.context.unbind("$");
3785 }
3786
3787 Ok(JValue::object(result))
3788 }
3789
3790 AstNode::Function {
3791 name,
3792 args,
3793 is_builtin,
3794 } => self.evaluate_function_call(name, args, *is_builtin, data),
3795
3796 // Call: invoke an arbitrary expression as a function
3797 // Used for IIFE patterns like (function($x){...})(5) or chained calls
3798 AstNode::Call { procedure, args } => {
3799 // Evaluate the procedure to get the callable value
3800 let callable = self.evaluate_internal(procedure, data)?;
3801
3802 // Check if it's a lambda value
3803 if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3804 let mut evaluated_args = Vec::with_capacity(args.len());
3805 for arg in args.iter() {
3806 evaluated_args.push(self.evaluate_internal(arg, data)?);
3807 }
3808 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3809 }
3810
3811 // Not a callable value
3812 Err(EvaluatorError::TypeError(format!(
3813 "Cannot call non-function value: {:?}",
3814 callable
3815 )))
3816 }
3817
3818 AstNode::Conditional {
3819 condition,
3820 then_branch,
3821 else_branch,
3822 } => {
3823 let condition_value = self.evaluate_internal(condition, data)?;
3824 if self.is_truthy(&condition_value) {
3825 self.evaluate_internal(then_branch, data)
3826 } else if let Some(else_branch) = else_branch {
3827 self.evaluate_internal(else_branch, data)
3828 } else {
3829 // No else branch - return undefined (not null)
3830 // This allows $map to filter out results from conditionals without else
3831 Ok(JValue::Undefined)
3832 }
3833 }
3834
3835 AstNode::Block(expressions) => {
3836 // Blocks create a new scope - push scope instead of clone/restore
3837 self.context.push_scope();
3838
3839 let mut result = JValue::Null;
3840 for expr in expressions {
3841 result = self.evaluate_internal(expr, data)?;
3842 }
3843
3844 // Before popping, preserve any lambdas referenced by the result
3845 // This is essential for closures returned from blocks (IIFE pattern)
3846 let lambdas_to_keep = self.extract_lambda_ids(&result);
3847 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3848
3849 Ok(result)
3850 }
3851
3852 // Lambda: capture current environment for closure support
3853 AstNode::Lambda {
3854 params,
3855 body,
3856 signature,
3857 thunk,
3858 } => {
3859 let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
3860
3861 let compiled_body = if !thunk {
3862 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3863 try_compile_expr_with_allowed_vars(body, &var_refs)
3864 } else {
3865 None
3866 };
3867 let stored_lambda = StoredLambda {
3868 params: params.clone(),
3869 body: (**body).clone(),
3870 compiled_body,
3871 signature: signature.clone(),
3872 captured_env: self.capture_environment_for(body, params),
3873 captured_data: Some(data.clone()),
3874 thunk: *thunk,
3875 };
3876 self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3877
3878 let lambda_obj = JValue::lambda(
3879 lambda_id.as_str(),
3880 params.clone(),
3881 None::<String>,
3882 signature.clone(),
3883 );
3884
3885 Ok(lambda_obj)
3886 }
3887
3888 // Wildcard: collect all values from current object
3889 AstNode::Wildcard => {
3890 match data {
3891 JValue::Object(obj) => {
3892 let mut result = Vec::new();
3893 for value in obj.values() {
3894 // Flatten arrays into the result
3895 match value {
3896 JValue::Array(arr) => result.extend(arr.iter().cloned()),
3897 _ => result.push(value.clone()),
3898 }
3899 }
3900 check_sequence_length(result.len(), &self.options)?;
3901 Ok(JValue::array(result))
3902 }
3903 JValue::Array(arr) => {
3904 // For arrays, wildcard returns all elements
3905 Ok(JValue::Array(arr.clone()))
3906 }
3907 _ => Ok(JValue::Null),
3908 }
3909 }
3910
3911 // Descendant: recursively traverse all nested values
3912 AstNode::Descendant => {
3913 let descendants = self.collect_descendants(data);
3914 if descendants.is_empty() {
3915 Ok(JValue::Null) // No descendants means undefined
3916 } else {
3917 check_sequence_length(descendants.len(), &self.options)?;
3918 Ok(JValue::array(descendants))
3919 }
3920 }
3921
3922 AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3923 "Predicate can only be used in path expressions".to_string(),
3924 )),
3925
3926 // Array grouping: same as Array but prevents flattening in path contexts
3927 AstNode::ArrayGroup(elements) => {
3928 let mut result = Vec::new();
3929 for element in elements {
3930 let value = self.evaluate_internal(element, data)?;
3931 result.push(value);
3932 }
3933 Ok(JValue::array(result))
3934 }
3935
3936 AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3937 "Function application can only be used in path expressions".to_string(),
3938 )),
3939
3940 AstNode::Sort { input, terms } => {
3941 // Keep the input path's tuple wrappers so the sort terms can read
3942 // the carried `%`/`$focus`/`$index` bindings per element.
3943 let saved = self.keep_tuple_stream;
3944 self.keep_tuple_stream = true;
3945 let value = self.evaluate_internal(input, data);
3946 self.keep_tuple_stream = saved;
3947 self.evaluate_sort(&value?, terms)
3948 }
3949
3950 // Transform: |location|update[,delete]|
3951 AstNode::Transform {
3952 location,
3953 update,
3954 delete,
3955 } => {
3956 // Check if $ is bound (meaning we're being invoked as a lambda)
3957 if self.context.lookup("$").is_some() {
3958 // Execute the transformation
3959 self.execute_transform(location, update, delete.as_deref(), data)
3960 } else {
3961 // Return a lambda representation
3962 // The transform will be executed when the lambda is invoked
3963 let transform_lambda = StoredLambda {
3964 params: vec!["$".to_string()],
3965 body: AstNode::Transform {
3966 location: location.clone(),
3967 update: update.clone(),
3968 delete: delete.clone(),
3969 },
3970 compiled_body: None, // Transform is not a pure compilable expr
3971 signature: None,
3972 captured_env: HashMap::new(),
3973 captured_data: None, // Transform takes $ as parameter
3974 thunk: false,
3975 };
3976
3977 // Store with a generated unique name
3978 let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
3979 self.context.bind_lambda(lambda_name, transform_lambda);
3980
3981 // Return lambda marker
3982 Ok(JValue::string("<lambda>"))
3983 }
3984 }
3985
3986 // Parent-reference operator (%): ast_transform has already resolved
3987 // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
3988 // tuple step binds that label into scope (create_tuple_stream +
3989 // needs_tuple_context_binding), so resolving it is an ordinary scope
3990 // lookup, mirroring jsonata-js's
3991 // `case 'parent': result = environment.lookup(expr.slot.label);`.
3992 AstNode::Parent(label) => {
3993 if let Some(v) = self.context.lookup(label) {
3994 return Ok(v.clone());
3995 }
3996 // Fall back to the tuple wrapper carried as `data`: a `%` used
3997 // inside a predicate/stage over a tuple stream -- e.g.
3998 // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
3999 // predicate is evaluated per tuple with the wrapper as data --
4000 // reads its ancestor from the tuple's `!label` key, which isn't
4001 // separately bound into scope here (mirrors AstNode::Variable's
4002 // tuple-binding fallback below).
4003 if let JValue::Object(obj) = data {
4004 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4005 if let Some(v) = obj.get(label) {
4006 return Ok(v.clone());
4007 }
4008 }
4009 }
4010 Ok(JValue::Undefined)
4011 }
4012 }
4013 }
4014
4015 /// Apply stages (filters/predicates) to a value during field extraction
4016 /// Non-array values are wrapped in an array before filtering (JSONata semantics)
4017 /// This matches the JavaScript reference where stages apply to sequences
4018 fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
4019 // Wrap non-arrays in an array for filtering (JSONata semantics)
4020 let mut result = match value {
4021 JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
4022 JValue::Array(_) => value,
4023 other => JValue::array(vec![other]),
4024 };
4025
4026 for stage in stages {
4027 match stage {
4028 Stage::Filter(predicate_expr) => {
4029 // When applying stages, use stage-specific predicate logic
4030 result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
4031 }
4032 // Positional index stages are meaningful only over a tuple stream
4033 // (they set a variable to each tuple's position); they are applied
4034 // in `create_tuple_stream`, not on a plain value sequence here.
4035 Stage::Index(_) => {}
4036 }
4037 }
4038 Ok(result)
4039 }
4040
4041 /// Check if an AST node is definitely a filter expression (comparison/logical)
4042 /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
4043 fn is_filter_predicate(predicate: &AstNode) -> bool {
4044 match predicate {
4045 AstNode::Binary { op, .. } => matches!(
4046 op,
4047 BinaryOp::GreaterThan
4048 | BinaryOp::GreaterThanOrEqual
4049 | BinaryOp::LessThan
4050 | BinaryOp::LessThanOrEqual
4051 | BinaryOp::Equal
4052 | BinaryOp::NotEqual
4053 | BinaryOp::And
4054 | BinaryOp::Or
4055 | BinaryOp::In
4056 ),
4057 AstNode::Unary {
4058 op: crate::ast::UnaryOp::Not,
4059 ..
4060 } => true,
4061 _ => false,
4062 }
4063 }
4064
4065 /// Evaluate a predicate as a stage during field extraction
4066 /// This has different semantics than standalone predicates:
4067 /// - Maps index operations over arrays of extracted values
4068 fn evaluate_predicate_as_stage(
4069 &mut self,
4070 current: &JValue,
4071 predicate: &AstNode,
4072 ) -> Result<JValue, EvaluatorError> {
4073 // Special case: empty brackets [] (represented as Boolean(true))
4074 if matches!(predicate, AstNode::Boolean(true)) {
4075 return match current {
4076 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
4077 JValue::Null => Ok(JValue::Null),
4078 other => Ok(JValue::array(vec![other.clone()])),
4079 };
4080 }
4081
4082 match current {
4083 JValue::Array(arr) => {
4084 // For stages: if we have an array of values (from field extraction),
4085 // apply the predicate to each value if appropriate
4086
4087 // Check if predicate is a numeric index
4088 if let AstNode::Number(n) = predicate {
4089 // Check if this is an array of arrays (extracted array fields)
4090 let is_array_of_arrays =
4091 arr.iter().any(|item| matches!(item, JValue::Array(_)));
4092
4093 if !is_array_of_arrays {
4094 // Simple values: just index normally
4095 return self.array_index(current, &JValue::Number(*n));
4096 }
4097
4098 // Array of arrays: map index access over each extracted array
4099 let mut result = Vec::new();
4100 for item in arr.iter() {
4101 match item {
4102 JValue::Array(_) => {
4103 let indexed = self.array_index(item, &JValue::Number(*n))?;
4104 if !indexed.is_null() && !indexed.is_undefined() {
4105 result.push(indexed);
4106 }
4107 }
4108 _ => {
4109 if *n == 0.0 {
4110 result.push(item.clone());
4111 }
4112 }
4113 }
4114 }
4115 return Ok(JValue::array(result));
4116 }
4117
4118 // Short-circuit: if predicate is definitely a comparison/logical expression,
4119 // skip speculative numeric evaluation and go directly to filter logic
4120 if Self::is_filter_predicate(predicate) {
4121 // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
4122 if let Some(compiled) = try_compile_expr(predicate) {
4123 let shape = arr.first().and_then(build_shape_cache);
4124 let mut filtered = Vec::with_capacity(arr.len());
4125 for item in arr.iter() {
4126 let result = if let Some(ref s) = shape {
4127 eval_compiled_shaped(
4128 &compiled,
4129 item,
4130 None,
4131 s,
4132 &self.options,
4133 self.start_time,
4134 )?
4135 } else {
4136 eval_compiled(
4137 &compiled,
4138 item,
4139 None,
4140 &self.options,
4141 self.start_time,
4142 )?
4143 };
4144 if compiled_is_truthy(&result) {
4145 filtered.push(item.clone());
4146 }
4147 }
4148 return Ok(JValue::array(filtered));
4149 }
4150 // Fallback: full AST evaluation
4151 let mut filtered = Vec::new();
4152 for item in arr.iter() {
4153 let item_result = self.evaluate_internal(predicate, item)?;
4154 if self.is_truthy(&item_result) {
4155 filtered.push(item.clone());
4156 }
4157 }
4158 return Ok(JValue::array(filtered));
4159 }
4160
4161 // Try to evaluate the predicate to see if it's a numeric index or array of indices
4162 // If evaluation succeeds and yields a number, use it as an index
4163 // If it yields an array of numbers, use them as multiple indices
4164 // If evaluation fails (e.g., comparison error), treat as filter
4165 match self.evaluate_internal(predicate, current) {
4166 Ok(JValue::Number(n)) => {
4167 let n_val = n;
4168 let is_array_of_arrays =
4169 arr.iter().any(|item| matches!(item, JValue::Array(_)));
4170
4171 if !is_array_of_arrays {
4172 let pred_result = JValue::Number(n_val);
4173 return self.array_index(current, &pred_result);
4174 }
4175
4176 // Array of arrays: map index access
4177 let mut result = Vec::new();
4178 let pred_result = JValue::Number(n_val);
4179 for item in arr.iter() {
4180 match item {
4181 JValue::Array(_) => {
4182 let indexed = self.array_index(item, &pred_result)?;
4183 if !indexed.is_null() && !indexed.is_undefined() {
4184 result.push(indexed);
4185 }
4186 }
4187 _ => {
4188 if n_val == 0.0 {
4189 result.push(item.clone());
4190 }
4191 }
4192 }
4193 }
4194 return Ok(JValue::array(result));
4195 }
4196 Ok(JValue::Array(indices)) => {
4197 // Array of values - could be indices or filter results
4198 // Check if all values are numeric
4199 let has_non_numeric =
4200 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
4201
4202 if has_non_numeric {
4203 // Non-numeric values - treat as filter, fall through
4204 } else {
4205 // All numeric - use as indices
4206 let arr_len = arr.len() as i64;
4207 let mut resolved_indices: Vec<i64> = indices
4208 .iter()
4209 .filter_map(|v| {
4210 if let JValue::Number(n) = v {
4211 let idx = *n as i64;
4212 // Resolve negative indices
4213 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
4214 // Only include valid indices
4215 if actual_idx >= 0 && actual_idx < arr_len {
4216 Some(actual_idx)
4217 } else {
4218 None
4219 }
4220 } else {
4221 None
4222 }
4223 })
4224 .collect();
4225
4226 // Sort and deduplicate indices
4227 resolved_indices.sort();
4228 resolved_indices.dedup();
4229
4230 // Select elements at each sorted index
4231 let result: Vec<JValue> = resolved_indices
4232 .iter()
4233 .map(|&idx| arr[idx as usize].clone())
4234 .collect();
4235
4236 return Ok(JValue::array(result));
4237 }
4238 }
4239 Ok(_) => {
4240 // Evaluated successfully but not a number or array - might be a filter
4241 // Fall through to filter logic
4242 }
4243 Err(_) => {
4244 // Evaluation failed - it's likely a filter expression
4245 // Fall through to filter logic
4246 }
4247 }
4248
4249 // It's a filter expression
4250 let mut filtered = Vec::new();
4251 for item in arr.iter() {
4252 let item_result = self.evaluate_internal(predicate, item)?;
4253 if self.is_truthy(&item_result) {
4254 filtered.push(item.clone());
4255 }
4256 }
4257 Ok(JValue::array(filtered))
4258 }
4259 JValue::Null => {
4260 // Null: return null
4261 Ok(JValue::Null)
4262 }
4263 other => {
4264 // Non-array values: treat as single-element conceptual array
4265 // For numeric predicates: index 0 returns the value, other indices return null
4266 // For boolean predicates: if truthy, return value; if falsy, return null
4267
4268 // Check if predicate is a numeric index
4269 if let AstNode::Number(n) = predicate {
4270 // Index 0 returns the value, other indices return null
4271 if *n == 0.0 {
4272 return Ok(other.clone());
4273 } else {
4274 return Ok(JValue::Null);
4275 }
4276 }
4277
4278 // Try to evaluate the predicate to see if it's a numeric index
4279 match self.evaluate_internal(predicate, other) {
4280 Ok(JValue::Number(n)) => {
4281 // Index 0 returns the value, other indices return null
4282 if n == 0.0 {
4283 Ok(other.clone())
4284 } else {
4285 Ok(JValue::Null)
4286 }
4287 }
4288 Ok(pred_result) => {
4289 // Boolean filter: return value if truthy, null if falsy
4290 if self.is_truthy(&pred_result) {
4291 Ok(other.clone())
4292 } else {
4293 Ok(JValue::Null)
4294 }
4295 }
4296 Err(e) => Err(e),
4297 }
4298 }
4299 }
4300 }
4301
4302 /// Evaluate a path expression (e.g., foo.bar.baz)
4303 fn evaluate_path(
4304 &mut self,
4305 steps: &[PathStep],
4306 data: &JValue,
4307 ) -> Result<JValue, EvaluatorError> {
4308 // Avoid cloning by using references and only cloning when necessary
4309 if steps.is_empty() {
4310 return Ok(data.clone());
4311 }
4312
4313 // Fast path: single field access on object
4314 // This is a very common pattern, so optimize it.
4315 // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
4316 // creation handled below.
4317 if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
4318 if let AstNode::Name(field_name) = &steps[0].node {
4319 return match data {
4320 JValue::Object(obj) => {
4321 // Check if this is a tuple - extract '@' value
4322 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4323 if let Some(JValue::Object(inner)) = obj.get("@") {
4324 Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
4325 } else {
4326 Ok(JValue::Undefined)
4327 }
4328 } else {
4329 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
4330 }
4331 }
4332 JValue::Array(arr) => {
4333 // Array mapping: extract field from each element
4334 // Optimized: use references to access fields without cloning entire objects
4335 // Check first element for tuple-ness (tuples are all-or-nothing)
4336 let has_tuples = arr.first().is_some_and(|item| {
4337 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4338 });
4339
4340 if !has_tuples {
4341 // Fast path: no tuples, just direct field lookups
4342 let mut result = Vec::with_capacity(arr.len());
4343 for item in arr.iter() {
4344 if let JValue::Object(obj) = item {
4345 if let Some(val) = obj.get(field_name) {
4346 if !val.is_null() {
4347 match val {
4348 JValue::Array(arr_val) => {
4349 result.extend(arr_val.iter().cloned());
4350 }
4351 other => result.push(other.clone()),
4352 }
4353 }
4354 }
4355 } else if let JValue::Array(inner_arr) = item {
4356 let nested_result = self.evaluate_path(
4357 &[PathStep::new(AstNode::Name(field_name.clone()))],
4358 &JValue::Array(inner_arr.clone()),
4359 )?;
4360 match nested_result {
4361 JValue::Array(nested) => {
4362 result.extend(nested.iter().cloned());
4363 }
4364 JValue::Null => {}
4365 other => result.push(other),
4366 }
4367 }
4368 }
4369
4370 if result.is_empty() {
4371 Ok(JValue::Null)
4372 } else if result.len() == 1 {
4373 Ok(result.into_iter().next().unwrap())
4374 } else {
4375 check_sequence_length(result.len(), &self.options)?;
4376 Ok(JValue::array(result))
4377 }
4378 } else {
4379 // Tuple path: per-element tuple handling
4380 let mut result = Vec::new();
4381 for item in arr.iter() {
4382 match item {
4383 JValue::Object(obj) => {
4384 let is_tuple =
4385 obj.get("__tuple__") == Some(&JValue::Bool(true));
4386
4387 if is_tuple {
4388 let inner = match obj.get("@") {
4389 Some(JValue::Object(inner)) => inner,
4390 _ => continue,
4391 };
4392
4393 if let Some(val) = inner.get(field_name) {
4394 if !val.is_null() {
4395 // Build tuple wrapper - only clone bindings when needed
4396 let wrap = |v: JValue| -> JValue {
4397 let mut wrapper = IndexMap::new();
4398 wrapper.insert("@".to_string(), v);
4399 wrapper.insert(
4400 "__tuple__".to_string(),
4401 JValue::Bool(true),
4402 );
4403 for (k, v) in obj.iter() {
4404 if k.starts_with('$') {
4405 wrapper
4406 .insert(k.clone(), v.clone());
4407 }
4408 }
4409 JValue::object(wrapper)
4410 };
4411
4412 match val {
4413 JValue::Array(arr_val) => {
4414 for item in arr_val.iter() {
4415 result.push(wrap(item.clone()));
4416 }
4417 }
4418 other => result.push(wrap(other.clone())),
4419 }
4420 }
4421 }
4422 } else {
4423 // Non-tuple: access field directly by reference, only clone the field value
4424 if let Some(val) = obj.get(field_name) {
4425 if !val.is_null() {
4426 match val {
4427 JValue::Array(arr_val) => {
4428 for item in arr_val.iter() {
4429 result.push(item.clone());
4430 }
4431 }
4432 other => result.push(other.clone()),
4433 }
4434 }
4435 }
4436 }
4437 }
4438 JValue::Array(inner_arr) => {
4439 // Recursively map over nested array
4440 let nested_result = self.evaluate_path(
4441 &[PathStep::new(AstNode::Name(field_name.clone()))],
4442 &JValue::Array(inner_arr.clone()),
4443 )?;
4444 // Add nested result to our results
4445 match nested_result {
4446 JValue::Array(nested) => {
4447 // Flatten nested arrays from recursive mapping
4448 result.extend(nested.iter().cloned());
4449 }
4450 JValue::Null => {} // Skip nulls from nested arrays
4451 other => result.push(other),
4452 }
4453 }
4454 _ => {} // Skip non-object items
4455 }
4456 }
4457
4458 // Return array result
4459 // JSONata singleton unwrapping: if we have exactly one result,
4460 // unwrap it (even if it's an array)
4461 if result.is_empty() {
4462 Ok(JValue::Null)
4463 } else if result.len() == 1 {
4464 Ok(result.into_iter().next().unwrap())
4465 } else {
4466 check_sequence_length(result.len(), &self.options)?;
4467 Ok(JValue::array(result))
4468 }
4469 } // end else (tuple path)
4470 }
4471 _ => Ok(JValue::Undefined),
4472 };
4473 }
4474 }
4475
4476 // Fast path: 2-step $variable.field with no stages
4477 // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4478 if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4479 if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4480 (&steps[0].node, &steps[1].node)
4481 {
4482 if !var_name.is_empty() {
4483 if let Some(value) = self.context.lookup(var_name) {
4484 match value {
4485 JValue::Object(obj) => {
4486 return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4487 }
4488 JValue::Array(arr) => {
4489 // Map field extraction over array (same as single-step Name on Array)
4490 let mut result = Vec::with_capacity(arr.len());
4491 for item in arr.iter() {
4492 if let JValue::Object(obj) = item {
4493 if let Some(val) = obj.get(field_name) {
4494 if !val.is_null() {
4495 match val {
4496 JValue::Array(inner) => {
4497 result.extend(inner.iter().cloned());
4498 }
4499 other => result.push(other.clone()),
4500 }
4501 }
4502 }
4503 }
4504 }
4505 return match result.len() {
4506 0 => Ok(JValue::Null),
4507 1 => Ok(result.pop().unwrap()),
4508 _ => {
4509 check_sequence_length(result.len(), &self.options)?;
4510 Ok(JValue::array(result))
4511 }
4512 };
4513 }
4514 _ => {} // Fall through to general path evaluation
4515 }
4516 }
4517 }
4518 }
4519 }
4520
4521 // Track whether we did array mapping (for singleton unwrapping)
4522 let mut did_array_mapping = false;
4523
4524 // For the first step, work with a reference.
4525 // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4526 // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4527 // first path step where tupleBindings is undefined.
4528 let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4529 JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4530 } else {
4531 match &steps[0].node {
4532 AstNode::Wildcard => {
4533 // Wildcard as first step
4534 match data {
4535 JValue::Object(obj) => {
4536 let mut result = Vec::new();
4537 for value in obj.values() {
4538 // Flatten arrays into the result
4539 match value {
4540 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4541 _ => result.push(value.clone()),
4542 }
4543 }
4544 JValue::array(result)
4545 }
4546 JValue::Array(arr) => JValue::Array(arr.clone()),
4547 _ => JValue::Null,
4548 }
4549 }
4550 AstNode::Descendant => {
4551 // Descendant as first step
4552 let descendants = self.collect_descendants(data);
4553 JValue::array(descendants)
4554 }
4555 AstNode::ParentVariable(name) => {
4556 // Parent variable as first step
4557 let parent_data = self.context.get_parent().ok_or_else(|| {
4558 EvaluatorError::ReferenceError("Parent context not available".to_string())
4559 })?;
4560
4561 if name.is_empty() {
4562 // $$ alone returns parent context
4563 parent_data.clone()
4564 } else {
4565 // $$field accesses field on parent
4566 match parent_data {
4567 JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4568 _ => JValue::Null,
4569 }
4570 }
4571 }
4572 AstNode::Name(field_name) => {
4573 // Field/property access - get the stages for this step
4574 let stages = &steps[0].stages;
4575
4576 match data {
4577 JValue::Object(obj) => {
4578 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4579 // Apply any stages to the extracted value
4580 if !stages.is_empty() {
4581 self.apply_stages(val, stages)?
4582 } else {
4583 val
4584 }
4585 }
4586 JValue::Array(arr) => {
4587 // Array mapping: extract field from each element and apply stages
4588 let mut result = Vec::new();
4589 for item in arr.iter() {
4590 match item {
4591 JValue::Object(obj) => {
4592 let val = obj
4593 .get(field_name)
4594 .cloned()
4595 .unwrap_or(JValue::Undefined);
4596 if !val.is_null() && !val.is_undefined() {
4597 if !stages.is_empty() {
4598 // Apply stages to the extracted value
4599 let processed_val =
4600 self.apply_stages(val, stages)?;
4601 // Stages always return an array (or null); extend results
4602 match processed_val {
4603 JValue::Array(arr) => {
4604 result.extend(arr.iter().cloned())
4605 }
4606 JValue::Null => {} // Skip nulls from stage application
4607 other => result.push(other), // Shouldn't happen, but handle it
4608 }
4609 } else {
4610 // No stages: flatten arrays, push scalars
4611 match val {
4612 JValue::Array(arr) => {
4613 result.extend(arr.iter().cloned())
4614 }
4615 other => result.push(other),
4616 }
4617 }
4618 }
4619 }
4620 JValue::Array(inner_arr) => {
4621 // Recursively map over nested array
4622 let nested_result = self.evaluate_path(
4623 &[steps[0].clone()],
4624 &JValue::Array(inner_arr.clone()),
4625 )?;
4626 match nested_result {
4627 JValue::Array(nested) => {
4628 result.extend(nested.iter().cloned())
4629 }
4630 JValue::Null => {} // Skip nulls from nested arrays
4631 other => result.push(other),
4632 }
4633 }
4634 _ => {} // Skip non-object items
4635 }
4636 }
4637 JValue::array(result)
4638 }
4639 JValue::Null => JValue::Null,
4640 // Accessing field on non-object returns undefined (not an error)
4641 _ => JValue::Undefined,
4642 }
4643 }
4644 AstNode::String(string_literal) => {
4645 // String literal in path context - evaluate as literal and apply stages
4646 // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4647 let stages = &steps[0].stages;
4648 let val = JValue::string(string_literal.clone());
4649
4650 if !stages.is_empty() {
4651 // Apply stages (predicates) to the string literal
4652 let result = self.apply_stages(val, stages)?;
4653 // Unwrap single-element arrays back to scalar
4654 // (string literals with predicates should return scalar or null, not arrays)
4655 match result {
4656 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4657 JValue::Array(arr) if arr.is_empty() => JValue::Null,
4658 other => other,
4659 }
4660 } else {
4661 val
4662 }
4663 }
4664 AstNode::Predicate(pred_expr) => {
4665 // Predicate as first step
4666 self.evaluate_predicate(data, pred_expr)?
4667 }
4668 _ => {
4669 // Complex first step - evaluate it. When the step is
4670 // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4671 // whose `Product` is `%`-tagged, as in
4672 // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4673 // inner path's tuple wrappers so the following predicate/step
4674 // can read the `!label` bindings.
4675 let saved_keep = self.keep_tuple_stream;
4676 if steps[0].is_tuple {
4677 self.keep_tuple_stream = true;
4678 }
4679 let v = self.evaluate_path_step(&steps[0].node, data, data);
4680 self.keep_tuple_stream = saved_keep;
4681 v?
4682 }
4683 }
4684 };
4685
4686 // Process remaining steps
4687 for (step_idx, step) in steps[1..].iter().enumerate() {
4688 let is_last_step = step_idx == steps.len() - 2;
4689 // Early return if current is null/undefined - no point continuing
4690 // This handles cases like `blah.{}` where blah doesn't exist
4691 if current.is_null() {
4692 return Ok(JValue::Null);
4693 }
4694 if current.is_undefined() {
4695 return Ok(JValue::Undefined);
4696 }
4697
4698 // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4699 // a tuple stream, which selects a single tuple and unwraps it out of
4700 // the array) must stay a tuple stream so the following step keeps
4701 // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4702 // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4703 if let JValue::Object(o) = ¤t {
4704 if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4705 current = JValue::array(vec![current.clone()]);
4706 // The lone wrapper came from a singleton index selection, so
4707 // the final result should unwrap back to a scalar (a following
4708 // object step must not leave a spurious 1-element array).
4709 did_array_mapping = true;
4710 }
4711 }
4712
4713 // Check if current is a tuple array - if so, we need to bind tuple variables
4714 // to context so they're available in nested expressions (like predicates)
4715 let is_tuple_array = if let JValue::Array(arr) = ¤t {
4716 arr.first().is_some_and(|first| {
4717 if let JValue::Object(obj) = first {
4718 obj.get("__tuple__") == Some(&JValue::Bool(true))
4719 } else {
4720 false
4721 }
4722 })
4723 } else {
4724 false
4725 };
4726
4727 // Tuple-binding step (@ focus / # index / % parent): create/extend the
4728 // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4729 // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4730 // wrappers via the existing tuple-aware handling below.
4731 //
4732 // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
4733 // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
4734 // stream, but ONLY when it is consuming an existing tuple stream:
4735 // its ancestor label lives in those incoming tuples, so
4736 // create_tuple_stream's per-tuple frame binding is what lets
4737 // `evaluate_internal(Parent, ..)` resolve it (and any predicate
4738 // stage on the `%` step then resolves in the same frame). A `%`
4739 // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
4740 // predicate, whose input is plain data, not a tuple stream) must
4741 // NOT be routed here -- it's an ordinary scope lookup.
4742 let is_parent_step_over_tuple =
4743 matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
4744 if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
4745 current = JValue::array(self.create_tuple_stream(step, ¤t, false)?);
4746 continue;
4747 }
4748
4749 // For tuple arrays with certain step types, we need special handling to bind
4750 // tuple variables to context so they're available in nested expressions.
4751 // This is needed for:
4752 // - Object constructors: {"label": $$.items[$i]} needs $i in context
4753 // - Function applications: .($$.items[$i]) needs $i in context
4754 // - Variable lookups: .$i needs to find the tuple binding
4755 //
4756 // Steps like Name (field access) already have proper tuple handling in their
4757 // specific cases, so we don't intercept those here.
4758 let needs_tuple_context_binding = is_tuple_array
4759 && matches!(
4760 &step.node,
4761 AstNode::Object(_)
4762 | AstNode::FunctionApplication(_)
4763 | AstNode::Variable(_)
4764 | AstNode::ArrayGroup(_)
4765 );
4766
4767 if needs_tuple_context_binding {
4768 if let JValue::Array(arr) = ¤t {
4769 let mut results = Vec::new();
4770
4771 for tuple in arr.iter() {
4772 if let JValue::Object(tuple_obj) = tuple {
4773 // Extract tuple bindings so nested expressions can see
4774 // them: `$var` focus/index bindings (stored `$name`,
4775 // bound as `name`) AND `!label` ancestor bindings for
4776 // `%` (stored and bound under the full `!label` key).
4777 // Saves/restores rather than blindly unbinding, so a
4778 // tuple key that collides with a live outer `:=`
4779 // binding doesn't get deleted afterward.
4780 let tuple_bindings = self.bind_tuple_keys(tuple_obj);
4781
4782 // Get the actual value from the tuple (@ field)
4783 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4784
4785 // Evaluate the step
4786 let step_result = match &step.node {
4787 AstNode::Variable(_) => {
4788 // Variable lookup - check context (which now has bindings)
4789 self.evaluate_internal(&step.node, tuple)?
4790 }
4791 AstNode::Object(_) | AstNode::ArrayGroup(_) => {
4792 // Object / array constructor step (e.g.
4793 // `Product.[`Product Name`, %.OrderID]`) -
4794 // evaluate on the tuple's `@` value with the
4795 // carried `!label`/`$focus` bindings in scope
4796 // so an embedded `%` resolves.
4797 self.evaluate_internal(&step.node, &actual_data)?
4798 }
4799 AstNode::FunctionApplication(inner) => {
4800 // A parenthesized step `(expr)` consuming a tuple stream
4801 // (e.g. `Account.Order.Product.( %.OrderID )` or
4802 // `Employee@$e.(Contact)[...]`): evaluate the INNER
4803 // expression on the tuple's `@` value with `$` bound to
4804 // it, mirroring the non-tuple FunctionApplication step
4805 // handling. Routing the wrapper node itself through
4806 // evaluate_internal raises "Function application can only
4807 // be used in path expressions".
4808 let saved_dollar = self.context.lookup("$").cloned();
4809 self.context.bind("$".to_string(), actual_data.clone());
4810 // Keep tuple wrappers from the inner path alive:
4811 // when `inner` is itself a tuple-carrying path
4812 // (e.g. `(Order.Product)` whose `Product` is
4813 // `%`-tagged), its `!label` wrappers must survive
4814 // to be merged into this tuple by the rewrap below
4815 // (they feed a later `%`/`%.%`). Without this the
4816 // inner path projects to `@` and drops the labels.
4817 let saved_keep = self.keep_tuple_stream;
4818 self.keep_tuple_stream = true;
4819 let v = self.evaluate_internal(inner, &actual_data);
4820 self.keep_tuple_stream = saved_keep;
4821 match saved_dollar {
4822 Some(s) => self.context.bind("$".to_string(), s),
4823 None => self.context.unbind("$"),
4824 }
4825 v?
4826 }
4827 _ => unreachable!(), // We only match specific types above
4828 };
4829
4830 // Apply this step's own filter stages (e.g. the
4831 // `[$substring(title,0,3)='The']` on `.$[...]` in
4832 // `library.books#$pos.$[...].$pos`) while the tuple
4833 // bindings are still in scope, so the predicate can
4834 // reference them and non-matching tuples are dropped.
4835 let step_result = if step.stages.is_empty() {
4836 step_result
4837 } else {
4838 self.apply_stages(step_result, &step.stages)?
4839 };
4840
4841 // Restore previous bindings
4842 tuple_bindings.restore(self);
4843
4844 // Rewrap results as tuples carrying this incoming
4845 // tuple's focus/index/ancestor bindings, so that
4846 // DOWNSTREAM steps keep seeing them: a predicate like
4847 // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
4848 // later `%`/`%.%` in `Account.Order.(Product).{...}`,
4849 // or a further path step all read those bindings from
4850 // the tuple wrapper (see AstNode::Variable's tuple
4851 // fallback). Without rewrapping, the tuple chain is
4852 // severed after a parenthesized/object/variable step
4853 // and those references resolve to nothing. The
4854 // wrappers are projected back to their `@` values by
4855 // the top-level `unwrap_tuple_output` pass.
4856 let carried: Vec<(String, JValue)> = tuple_obj
4857 .iter()
4858 .filter(|(k, _)| {
4859 (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
4860 })
4861 .map(|(k, v)| (k.clone(), v.clone()))
4862 .collect();
4863 let wrap = |v: JValue| -> JValue {
4864 match v {
4865 // If the step produced a nested tuple stream
4866 // (e.g. `(Product)` whose inner `Product` is
4867 // itself `%`-tagged), MERGE the inner tuple's
4868 // keys over the carried outer bindings, mirroring
4869 // jsonata-js's `res.tupleStream` branch
4870 // (`Object.assign(tuple, res[bb])`) -- do NOT
4871 // double-wrap, which would bury `@`/`!label`
4872 // one level down and break a following `%`/`%.%`.
4873 JValue::Object(inner)
4874 if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
4875 {
4876 let mut w = IndexMap::new();
4877 for (k, val) in &carried {
4878 w.insert(k.clone(), val.clone());
4879 }
4880 for (k, val) in inner.iter() {
4881 w.insert(k.clone(), val.clone());
4882 }
4883 w.insert("__tuple__".to_string(), JValue::Bool(true));
4884 JValue::object(w)
4885 }
4886 other => {
4887 let mut w = IndexMap::new();
4888 w.insert("@".to_string(), other);
4889 for (k, val) in &carried {
4890 w.insert(k.clone(), val.clone());
4891 }
4892 w.insert("__tuple__".to_string(), JValue::Bool(true));
4893 JValue::object(w)
4894 }
4895 }
4896 };
4897 if !step_result.is_null() && !step_result.is_undefined() {
4898 // Object constructors yield one value per tuple;
4899 // other steps may yield an array to splice in.
4900 if matches!(&step.node, AstNode::Object(_)) {
4901 results.push(wrap(step_result));
4902 } else if let JValue::Array(arr) = step_result {
4903 for it in arr.iter() {
4904 results.push(wrap(it.clone()));
4905 }
4906 } else {
4907 results.push(wrap(step_result));
4908 }
4909 }
4910 }
4911 }
4912
4913 current = JValue::array(results);
4914 continue; // Skip the regular step processing
4915 }
4916 }
4917
4918 current = match &step.node {
4919 AstNode::Wildcard => {
4920 // Wildcard in path
4921 let stages = &step.stages;
4922 let wildcard_result = match ¤t {
4923 JValue::Object(obj) => {
4924 let mut result = Vec::new();
4925 for value in obj.values() {
4926 // Flatten arrays into the result
4927 match value {
4928 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4929 _ => result.push(value.clone()),
4930 }
4931 }
4932 JValue::array(result)
4933 }
4934 JValue::Array(arr) => {
4935 // Map wildcard over array
4936 let mut all_values = Vec::new();
4937 for item in arr.iter() {
4938 match item {
4939 JValue::Object(obj) => {
4940 for value in obj.values() {
4941 // Flatten arrays
4942 match value {
4943 JValue::Array(arr) => {
4944 all_values.extend(arr.iter().cloned())
4945 }
4946 _ => all_values.push(value.clone()),
4947 }
4948 }
4949 }
4950 JValue::Array(inner) => {
4951 all_values.extend(inner.iter().cloned());
4952 }
4953 _ => {}
4954 }
4955 }
4956 JValue::array(all_values)
4957 }
4958 _ => JValue::Null,
4959 };
4960
4961 // Apply stages (predicates) if present
4962 if !stages.is_empty() {
4963 self.apply_stages(wildcard_result, stages)?
4964 } else {
4965 wildcard_result
4966 }
4967 }
4968 AstNode::Descendant => {
4969 // Descendant in path
4970 match ¤t {
4971 JValue::Array(arr) => {
4972 // Collect descendants from all array elements
4973 let mut all_descendants = Vec::new();
4974 for item in arr.iter() {
4975 all_descendants.extend(self.collect_descendants(item));
4976 }
4977 JValue::array(all_descendants)
4978 }
4979 _ => {
4980 // Collect descendants from current value
4981 let descendants = self.collect_descendants(¤t);
4982 JValue::array(descendants)
4983 }
4984 }
4985 }
4986 AstNode::Name(field_name) => {
4987 // Navigate into object field or map over array, applying stages
4988 let stages = &step.stages;
4989
4990 match ¤t {
4991 JValue::Object(obj) => {
4992 // Single object field extraction - NOT array mapping
4993 // This resets did_array_mapping because we're extracting from
4994 // a single value, not mapping over an array. The field's value
4995 // (even if it's an array) should be preserved as-is.
4996 did_array_mapping = false;
4997 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4998 // Apply stages if present
4999 if !stages.is_empty() {
5000 self.apply_stages(val, stages)?
5001 } else {
5002 val
5003 }
5004 }
5005 JValue::Array(arr) => {
5006 // Array mapping: extract field from each element and apply stages
5007 did_array_mapping = true; // Track that we did array mapping
5008
5009 // Fast path: if no elements are tuples and no stages,
5010 // skip all tuple checking overhead (common case for products.price etc.)
5011 // Tuples are all-or-nothing (created by index binding #$i),
5012 // so checking only the first element is sufficient.
5013 let has_tuples = arr.first().is_some_and(|item| {
5014 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
5015 });
5016
5017 if !has_tuples && stages.is_empty() {
5018 let mut result = Vec::with_capacity(arr.len());
5019 for item in arr.iter() {
5020 match item {
5021 JValue::Object(obj) => {
5022 if let Some(val) = obj.get(field_name) {
5023 if !val.is_null() {
5024 match val {
5025 JValue::Array(arr_val) => {
5026 result.extend(arr_val.iter().cloned())
5027 }
5028 other => result.push(other.clone()),
5029 }
5030 }
5031 }
5032 }
5033 JValue::Array(_) => {
5034 let nested_result =
5035 self.evaluate_path(&[step.clone()], item)?;
5036 match nested_result {
5037 JValue::Array(nested) => {
5038 result.extend(nested.iter().cloned())
5039 }
5040 JValue::Null => {}
5041 other => result.push(other),
5042 }
5043 }
5044 _ => {}
5045 }
5046 }
5047 JValue::array(result)
5048 } else {
5049 // Full path with tuple support and stages
5050 let mut result = Vec::new();
5051
5052 for item in arr.iter() {
5053 match item {
5054 JValue::Object(obj) => {
5055 // Check if this is a tuple stream element
5056 let (actual_obj, tuple_bindings) = if obj
5057 .get("__tuple__")
5058 == Some(&JValue::Bool(true))
5059 {
5060 // This is a tuple - extract '@' value and preserve bindings
5061 if let Some(JValue::Object(inner)) = obj.get("@") {
5062 // Collect index bindings (variables starting with $)
5063 let bindings: Vec<(String, JValue)> = obj
5064 .iter()
5065 .filter(|(k, _)| k.starts_with('$'))
5066 .map(|(k, v)| (k.clone(), v.clone()))
5067 .collect();
5068 (inner.clone(), Some(bindings))
5069 } else {
5070 continue; // Invalid tuple
5071 }
5072 } else {
5073 (obj.clone(), None)
5074 };
5075
5076 let val = actual_obj
5077 .get(field_name)
5078 .cloned()
5079 .unwrap_or(JValue::Null);
5080
5081 if !val.is_null() {
5082 // Helper to wrap value in tuple if we have bindings
5083 let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
5084 if let Some(b) = bindings {
5085 let mut wrapper = IndexMap::new();
5086 wrapper.insert("@".to_string(), v);
5087 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5088 for (k, val) in b {
5089 wrapper.insert(k.clone(), val.clone());
5090 }
5091 JValue::object(wrapper)
5092 } else {
5093 v
5094 }
5095 };
5096
5097 if !stages.is_empty() {
5098 // Bind this tuple's carried focus/index/ancestor
5099 // bindings so a filter predicate that references
5100 // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
5101 // where the `[$l.isbn=isbn]` stage on the (non-focus)
5102 // `books` step must see `$l` from the enclosing
5103 // `@$l` focus stream. Without this the predicate
5104 // evaluates `$l` as unbound and filters everything out.
5105 let saved_tuple: Vec<(String, Option<JValue>)> =
5106 obj.iter()
5107 .filter_map(|(k, _)| {
5108 if let Some(n) = k.strip_prefix('$')
5109 {
5110 (!n.is_empty())
5111 .then(|| n.to_string())
5112 } else if k.starts_with('!') {
5113 Some(k.clone())
5114 } else {
5115 None
5116 }
5117 })
5118 .map(|n| {
5119 (
5120 n.clone(),
5121 self.context
5122 .lookup(&n)
5123 .cloned(),
5124 )
5125 })
5126 .collect();
5127 for (k, v) in obj.iter() {
5128 if let Some(n) = k.strip_prefix('$') {
5129 if !n.is_empty() {
5130 self.context
5131 .bind(n.to_string(), v.clone());
5132 }
5133 } else if k.starts_with('!') {
5134 self.context.bind(k.clone(), v.clone());
5135 }
5136 }
5137 // Apply stages to the extracted value
5138 let processed_val =
5139 self.apply_stages(val, stages);
5140 for (n, old) in saved_tuple.into_iter().rev() {
5141 match old {
5142 Some(v) => self.context.bind(n, v),
5143 None => self.context.unbind(&n),
5144 }
5145 }
5146 let processed_val = processed_val?;
5147 // Stages always return an array (or null); extend results
5148 match processed_val {
5149 JValue::Array(arr) => {
5150 for item in arr.iter() {
5151 result.push(wrap_in_tuple(
5152 item.clone(),
5153 &tuple_bindings,
5154 ));
5155 }
5156 }
5157 JValue::Null => {} // Skip nulls from stage application
5158 other => result.push(wrap_in_tuple(
5159 other,
5160 &tuple_bindings,
5161 )),
5162 }
5163 } else {
5164 // No stages: flatten arrays, push scalars
5165 // But preserve tuple bindings!
5166 match val {
5167 JValue::Array(arr) => {
5168 for item in arr.iter() {
5169 result.push(wrap_in_tuple(
5170 item.clone(),
5171 &tuple_bindings,
5172 ));
5173 }
5174 }
5175 other => result.push(wrap_in_tuple(
5176 other,
5177 &tuple_bindings,
5178 )),
5179 }
5180 }
5181 }
5182 }
5183 JValue::Array(_) => {
5184 // Recursively map over nested array
5185 let nested_result =
5186 self.evaluate_path(&[step.clone()], item)?;
5187 match nested_result {
5188 JValue::Array(nested) => {
5189 result.extend(nested.iter().cloned())
5190 }
5191 JValue::Null => {}
5192 other => result.push(other),
5193 }
5194 }
5195 _ => {}
5196 }
5197 }
5198
5199 JValue::array(result)
5200 }
5201 }
5202 JValue::Null => JValue::Null,
5203 // Accessing field on non-object returns undefined (not an error)
5204 _ => JValue::Undefined,
5205 }
5206 }
5207 AstNode::String(string_literal) => {
5208 // String literal as a path step - evaluate as literal and apply stages
5209 let stages = &step.stages;
5210 let val = JValue::string(string_literal.clone());
5211
5212 if !stages.is_empty() {
5213 // Apply stages (predicates) to the string literal
5214 let result = self.apply_stages(val, stages)?;
5215 // Unwrap single-element arrays back to scalar
5216 match result {
5217 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
5218 JValue::Array(arr) if arr.is_empty() => JValue::Null,
5219 other => other,
5220 }
5221 } else {
5222 val
5223 }
5224 }
5225 AstNode::Predicate(pred_expr) => {
5226 // Predicate in path - filter or index into current value
5227 self.evaluate_predicate(¤t, pred_expr)?
5228 }
5229 AstNode::ArrayGroup(elements) => {
5230 // Array grouping: map expression over array but keep results grouped
5231 // .[expr] means evaluate expr for each array element
5232 match ¤t {
5233 JValue::Array(arr) => {
5234 let mut result = Vec::new();
5235 for item in arr.iter() {
5236 // For each array item, evaluate all elements and collect results
5237 let mut group_values = Vec::new();
5238 for element in elements {
5239 let value = self.evaluate_internal(element, item)?;
5240 // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
5241 // This ensures [[expr]] produces properly nested arrays
5242 let should_preserve_array = matches!(
5243 element,
5244 AstNode::Array(_) | AstNode::ArrayGroup(_)
5245 );
5246
5247 if should_preserve_array {
5248 // Keep the array as a single element to preserve nesting
5249 group_values.push(value);
5250 } else {
5251 // Flatten the value into group_values
5252 match value {
5253 JValue::Array(arr) => {
5254 group_values.extend(arr.iter().cloned())
5255 }
5256 other => group_values.push(other),
5257 }
5258 }
5259 }
5260 // Each array element gets its own sub-array with all values
5261 result.push(JValue::array(group_values));
5262 }
5263 // jsonata-js's evaluateStep: when this is the path's last
5264 // step and mapping produced exactly one constructed
5265 // sub-array, that sub-array IS the path result directly
5266 // (not wrapped in an outer singleton array) — e.g.
5267 // `$.[value,epochSeconds]` over a 1-element array yields
5268 // `[3, 1578381600]`, not `[[3, 1578381600]]`.
5269 if is_last_step && result.len() == 1 {
5270 result.into_iter().next().unwrap()
5271 } else {
5272 JValue::array(result)
5273 }
5274 }
5275 _ => {
5276 // For non-arrays, just evaluate the array constructor normally
5277 let mut result = Vec::new();
5278 for element in elements {
5279 let value = self.evaluate_internal(element, ¤t)?;
5280 result.push(value);
5281 }
5282 JValue::array(result)
5283 }
5284 }
5285 }
5286 AstNode::FunctionApplication(expr) => {
5287 // Function application: map expr over the current value
5288 // .(expr) means evaluate expr for each element, with $ bound to that element
5289 // Null/undefined results are filtered out
5290 //
5291 // When this parenthesized step is itself tuple-carrying (its
5292 // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
5293 // keep the inner path's tuple wrappers so their `!label`
5294 // bindings survive to the following object/`%` step; the
5295 // end-of-path projection (or a later consumer) unwraps them.
5296 let saved_keep = self.keep_tuple_stream;
5297 if step.is_tuple {
5298 self.keep_tuple_stream = true;
5299 }
5300 let fa_result = match ¤t {
5301 JValue::Array(arr) => {
5302 // Produce the mapped result (compiled fast path or tree-walker fallback).
5303 // Do NOT return early — singleton unwrapping is applied by the outer
5304 // path evaluation code after all steps are processed.
5305 let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
5306 {
5307 let shape = arr.first().and_then(build_shape_cache);
5308 let mut result = Vec::with_capacity(arr.len());
5309 for item in arr.iter() {
5310 let value = if let Some(ref s) = shape {
5311 eval_compiled_shaped(
5312 &compiled,
5313 item,
5314 None,
5315 s,
5316 &self.options,
5317 self.start_time,
5318 )?
5319 } else {
5320 eval_compiled(
5321 &compiled,
5322 item,
5323 None,
5324 &self.options,
5325 self.start_time,
5326 )?
5327 };
5328 if !value.is_null() && !value.is_undefined() {
5329 result.push(value);
5330 }
5331 }
5332 result
5333 } else {
5334 let mut result = Vec::new();
5335 for item in arr.iter() {
5336 // Save the current $ binding
5337 let saved_dollar = self.context.lookup("$").cloned();
5338
5339 // Bind $ to the current item
5340 self.context.bind("$".to_string(), item.clone());
5341
5342 // Evaluate the expression in the context of this item
5343 let value = self.evaluate_internal(expr, item)?;
5344
5345 // Restore the previous $ binding
5346 if let Some(saved) = saved_dollar {
5347 self.context.bind("$".to_string(), saved);
5348 } else {
5349 self.context.unbind("$");
5350 }
5351
5352 // Only include non-null/undefined values
5353 if !value.is_null() && !value.is_undefined() {
5354 result.push(value);
5355 }
5356 }
5357 result
5358 };
5359 // Don't do singleton unwrapping here - let the path result
5360 // handling deal with it, which respects has_explicit_array_keep
5361 JValue::array(mapped)
5362 }
5363 _ => {
5364 // For non-arrays, bind $ and evaluate
5365 let saved_dollar = self.context.lookup("$").cloned();
5366 self.context.bind("$".to_string(), current.clone());
5367
5368 let value = self.evaluate_internal(expr, ¤t)?;
5369
5370 if let Some(saved) = saved_dollar {
5371 self.context.bind("$".to_string(), saved);
5372 } else {
5373 self.context.unbind("$");
5374 }
5375
5376 value
5377 }
5378 };
5379 self.keep_tuple_stream = saved_keep;
5380 fa_result
5381 }
5382 AstNode::Sort { terms, .. } => {
5383 // Sort as a path step - sort 'current' by the terms
5384 self.evaluate_sort(¤t, terms)?
5385 }
5386 // Handle complex path steps (e.g., computed properties, object construction)
5387 _ => {
5388 let saved_keep = self.keep_tuple_stream;
5389 if step.is_tuple {
5390 self.keep_tuple_stream = true;
5391 }
5392 let v = self.evaluate_path_step(&step.node, ¤t, data);
5393 self.keep_tuple_stream = saved_keep;
5394 v?
5395 }
5396 };
5397 }
5398
5399 // End-of-path tuple projection, mirroring jsonata-js evaluatePath
5400 // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
5401 // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
5402 // wrappers are internal bookkeeping and must not escape into an enclosing
5403 // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
5404 // `=` compare wrapper objects and always yield false). Suppressed only for
5405 // the two consumers that read the carried bindings directly off the
5406 // wrappers (Sort input, ObjectTransform/group-by input), which set
5407 // `keep_tuple_stream`. The top-level `evaluate()` still runs
5408 // `unwrap_tuple_output` as a backstop for wrappers nested inside
5409 // constructed output.
5410 if !self.keep_tuple_stream {
5411 if let JValue::Array(arr) = ¤t {
5412 let is_tuple_stream = arr.first().is_some_and(|f| {
5413 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5414 });
5415 if is_tuple_stream {
5416 let projected: Vec<JValue> = arr
5417 .iter()
5418 .map(|t| match t {
5419 JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
5420 other => other.clone(),
5421 })
5422 .collect();
5423 current = JValue::array(projected);
5424 }
5425 }
5426 }
5427
5428 // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5429 // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5430
5431 // Check for explicit array-keeping operations. Empty predicate `[]` can
5432 // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5433 // stage; it also counts when it sits inside a `Sort` step's input path
5434 // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5435 // the sort and the trailing index so the singleton stays `[4]`.
5436 let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5437
5438 // Unwrap when:
5439 // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5440 // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5441 // Note: did_array_mapping is reset to false when extracting from a single object,
5442 // so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5443 // BUT NOT when there's an explicit array-keeping operation
5444 //
5445 // Important: We DON'T unwrap just because original data was an array - what matters is
5446 // whether the final extraction was from an array mapping context or a single object.
5447 let should_unwrap = !has_explicit_array_keep
5448 && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5449
5450 let result = match ¤t {
5451 // An empty result sequence is "no value" -> undefined (jsonata-js
5452 // treats an empty sequence, e.g. from a filter that matched nothing,
5453 // as undefined so a following `.field` and object/array construction
5454 // drop it rather than keeping an explicit null). `[]` array-keep is
5455 // handled separately above via has_explicit_array_keep.
5456 JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5457 // Unwrap singleton arrays when appropriate
5458 JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5459 // Keep arrays otherwise
5460 _ => current,
5461 };
5462
5463 // An explicit `[]` keep-array forces the result to remain an array even
5464 // after a later singleton index collapses it to a scalar (jsonata's
5465 // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5466 let result = if has_explicit_array_keep
5467 && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5468 {
5469 JValue::array(vec![result])
5470 } else {
5471 result
5472 };
5473
5474 if let JValue::Array(arr) = &result {
5475 check_sequence_length(arr.len(), &self.options)?;
5476 }
5477
5478 Ok(result)
5479 }
5480
5481 /// True when a path step carries a tuple-binding flag (`@$var` focus,
5482 /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5483 /// produce/extend a tuple stream rather than be evaluated as a plain step.
5484 ///
5485 fn step_creates_tuple(step: &PathStep) -> bool {
5486 step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5487 }
5488
5489 /// True when a path contains an explicit empty predicate `[]` (keep-array),
5490 /// either directly as a step/stage or nested inside a `Sort` step's input
5491 /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5492 /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5493 /// -> `[4]`).
5494 fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5495 steps.iter().any(|step| {
5496 if let AstNode::Predicate(pred) = &step.node {
5497 if matches!(**pred, AstNode::Boolean(true)) {
5498 return true;
5499 }
5500 }
5501 if step.stages.iter().any(
5502 |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5503 ) {
5504 return true;
5505 }
5506 if let AstNode::Sort { input, .. } = &step.node {
5507 if let AstNode::Path { steps: inner } = input.as_ref() {
5508 return Self::path_keeps_singleton_array(inner);
5509 }
5510 }
5511 false
5512 })
5513 }
5514
5515 /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5516 /// scope, saving whatever was previously bound under each of those names
5517 /// so [`TupleKeyBindings::restore`] can put it back afterward.
5518 ///
5519 /// This is the single shared implementation of the
5520 /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5521 /// pattern that recurs across `create_tuple_stream`,
5522 /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5523 /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5524 /// none of those call sites can regress to a blind `unbind` (which
5525 /// deletes rather than restores a same-named outer `:=` binding that was
5526 /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5527 /// binding silently clobbering an outer variable of the same name).
5528 fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5529 let mut saved = Vec::new();
5530 for (key, value) in tuple_obj.iter() {
5531 let name = if let Some(n) = key.strip_prefix('$') {
5532 if n.is_empty() {
5533 continue;
5534 }
5535 n.to_string()
5536 } else if key.starts_with('!') {
5537 key.clone()
5538 } else {
5539 continue;
5540 };
5541 saved.push((name.clone(), self.context.lookup(&name).cloned()));
5542 self.context.bind(name, value.clone());
5543 }
5544 TupleKeyBindings { saved }
5545 }
5546
5547 /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5548 /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5549 /// vector holds `JValue::Object` tuple wrappers of the shape
5550 /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5551 /// which downstream steps consume via the existing tuple-aware handling in
5552 /// `evaluate_path`.
5553 ///
5554 /// `input` is the previous step's result: either an already-built tuple
5555 /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5556 /// plain value/array entering tuple mode for the first time (each item
5557 /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5558 ///
5559 /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5560 /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5561 /// fast paths only *rebuild* a wrapper around a value pulled from an input
5562 /// element that is already `__tuple__`-tagged, which can only be true if a
5563 /// `create_tuple_stream` call already ran earlier in this evaluation and set
5564 /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5565 /// fire on a value that did NOT come from an existing tuple stream, it must
5566 /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5567 /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5568 fn create_tuple_stream(
5569 &mut self,
5570 step: &PathStep,
5571 input: &JValue,
5572 is_first_path_step: bool,
5573 ) -> Result<Vec<JValue>, EvaluatorError> {
5574 use std::rc::Rc;
5575
5576 // Mark that this evaluate() call produced tuple wrappers, so the
5577 // top-level `evaluate()` knows to run the output-unwrap pass.
5578 self.tuple_stream_created = true;
5579
5580 // Gather the incoming tuple bindings.
5581 let is_tuple_input = matches!(
5582 input,
5583 JValue::Array(arr) if arr.first().is_some_and(|f| {
5584 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5585 })
5586 );
5587 let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5588 match input {
5589 JValue::Array(arr) => arr
5590 .iter()
5591 .filter_map(|t| match t {
5592 JValue::Object(o) => Some(o.clone()),
5593 _ => None,
5594 })
5595 .collect(),
5596 _ => unreachable!(),
5597 }
5598 } else {
5599 let items: Vec<JValue> = match input {
5600 // Mirrors jsonata-js evaluatePath's inputSequence rule
5601 // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5602 // when the path's FIRST step is a variable reference (`$`/`$$`) the
5603 // input array is taken as a SINGLE sequence value
5604 // (`createSequence(input)`) rather than iterated per-element. We
5605 // only need this for a leading INDEX bind (`$#$pos`): the whole
5606 // array becomes one incoming tuple whose `@` is the array, then
5607 // the inner position counter walks its elements so `$pos` runs
5608 // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5609 // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5610 // the step input, so a single binding would yield one copy of the
5611 // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5612 // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5613 // `$.$#$pos` (a later step) still iterates per-element.
5614 JValue::Array(arr)
5615 if !(is_first_path_step
5616 && matches!(&step.node, AstNode::Variable(_))
5617 && step.index_var.is_some()) =>
5618 {
5619 arr.iter().cloned().collect()
5620 }
5621 single => vec![single.clone()],
5622 };
5623 items
5624 .into_iter()
5625 .map(|item| {
5626 let mut wrapper = IndexMap::new();
5627 wrapper.insert("@".to_string(), item);
5628 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5629 Rc::new(wrapper)
5630 })
5631 .collect()
5632 };
5633
5634 // A sort step in a tuple stream orders the WHOLE stream (not per element)
5635 // and re-tuples with the index = sorted position, mirroring jsonata-js
5636 // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5637 // array, then number the sorted values, then filter by `$pos`.
5638 if let AstNode::Sort { terms, .. } = &step.node {
5639 let stream = JValue::array(
5640 incoming
5641 .iter()
5642 .map(|t| JValue::object((**t).clone()))
5643 .collect(),
5644 );
5645 // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5646 // carried keys bound), returning the wrappers in sorted order.
5647 let sorted = self.evaluate_sort(&stream, terms)?;
5648 let sorted_arr: Vec<JValue> = match sorted {
5649 JValue::Array(a) => a.iter().cloned().collect(),
5650 JValue::Null | JValue::Undefined => Vec::new(),
5651 other => vec![other],
5652 };
5653 let mut result = Vec::new();
5654 for (ss, elem) in sorted_arr.into_iter().enumerate() {
5655 let mut new_tuple = match elem {
5656 JValue::Object(o) => (*o).clone(),
5657 other => {
5658 let mut m = IndexMap::new();
5659 m.insert("@".to_string(), other);
5660 m
5661 }
5662 };
5663 if let Some(index_var) = &step.index_var {
5664 new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
5665 }
5666 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5667 result.push(JValue::object(new_tuple));
5668 }
5669 return Ok(result);
5670 }
5671
5672 let mut result = Vec::new();
5673 for tuple_obj in incoming {
5674 // Bind every carried tuple key into a real scope frame so the step
5675 // expression can see prior focus/index/ancestor bindings, mirroring
5676 // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
5677 // Saves/restores rather than blindly unbinding, so a tuple key
5678 // whose name collides with a live outer `:=` binding doesn't get
5679 // deleted once this tuple row's evaluation is done.
5680 let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
5681
5682 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
5683 let step_value = self.evaluate_internal(&step.node, &actual_data);
5684
5685 let mut step_value = step_value?;
5686 // When the step carries an ORDERED index stage (a second `#$var`,
5687 // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
5688 // BUILT tuple stream in order (filter then re-number) so the filter
5689 // sees the per-tuple focus/index bindings and each index reflects the
5690 // position at its point in the sequence. Those steps defer all stage
5691 // application to `apply_tuple_stages` after the stream is built.
5692 let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
5693 if !step.stages.is_empty() && !has_index_stage {
5694 // A `%` inside a filter predicate refers to the ancestry of
5695 // THIS step (its own input for a level-1 `%`, or an earlier
5696 // step's input for a `%.%` chain). ast_transform tags this step
5697 // with `ancestor_label`; bind it to the step's input so the
5698 // level-1 `%` resolves. The `%.%` chain's deeper references use
5699 // labels carried in the INCOMING tuple, so those bindings
5700 // (`tuple_bindings`) must stay live through `apply_stages` --
5701 // their restore is deferred until after it (previously they
5702 // were unbound first, which silently broke `%.%` inside
5703 // predicates).
5704 let own_label = match &step.ancestor_label {
5705 Some(label) if !tuple_bindings.contains(label) => {
5706 self.context.bind(label.clone(), actual_data.clone());
5707 Some(label.clone())
5708 }
5709 _ => None,
5710 };
5711 step_value = self.apply_stages(step_value, &step.stages)?;
5712 if let Some(label) = own_label {
5713 self.context.unbind(&label);
5714 }
5715 }
5716
5717 tuple_bindings.restore(self);
5718
5719 let row: Vec<JValue> = match step_value {
5720 JValue::Undefined => continue,
5721 JValue::Array(arr) => arr.iter().cloned().collect(),
5722 other => vec![other],
5723 };
5724
5725 for (position, value) in row.into_iter().enumerate() {
5726 if value.is_undefined() {
5727 continue;
5728 }
5729 let mut new_tuple = (*tuple_obj).clone();
5730 if let Some(focus_var) = &step.focus {
5731 // Focus binding keeps `@` as this step's INPUT (already carried
5732 // in the cloned tuple) and binds the result to `$focus`,
5733 // matching jsonata-js: `tuple[expr.focus] = res[bb];
5734 // tuple['@'] = tupleBindings[ee]['@'];`.
5735 new_tuple.insert(format!("${}", focus_var), value);
5736 } else {
5737 new_tuple.insert("@".to_string(), value);
5738 }
5739 if let Some(index_var) = &step.index_var {
5740 // Index binding records the position of this value WITHIN the
5741 // per-binding result row (jsonata-js evaluateTupleStep: the
5742 // inner `bb` counter, `tuple[expr.index] = bb`), which resets
5743 // for each incoming tuple.
5744 new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
5745 }
5746 if let Some(ancestor_label) = &step.ancestor_label {
5747 // `%` ancestor: preserve this step's INPUT under the label.
5748 new_tuple.insert(ancestor_label.clone(), actual_data.clone());
5749 }
5750 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5751 result.push(JValue::object(new_tuple));
5752 }
5753 }
5754
5755 // Apply ordered filter/index stages to the built tuple stream when a
5756 // second index binding deferred them (see the has_index_stage comment
5757 // in the build loop above).
5758 if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
5759 result = self.apply_tuple_stages(result, &step.stages)?;
5760 }
5761
5762 Ok(result)
5763 }
5764
5765 /// Apply a step's stages, in order, to an already-built tuple stream --
5766 /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
5767 /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
5768 /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
5769 /// stage sets its variable on every surviving tuple to that tuple's position
5770 /// in the CURRENT stream. Used for steps carrying a second `#$var` index
5771 /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
5772 /// pre-filter position and `$ib2` the post-filter position.
5773 fn apply_tuple_stages(
5774 &mut self,
5775 mut tuples: Vec<JValue>,
5776 stages: &[Stage],
5777 ) -> Result<Vec<JValue>, EvaluatorError> {
5778 for stage in stages {
5779 match stage {
5780 Stage::Filter(pred) => {
5781 let mut kept = Vec::with_capacity(tuples.len());
5782 for tup in tuples.into_iter() {
5783 let JValue::Object(obj) = &tup else {
5784 continue;
5785 };
5786 // Bind this tuple's carried focus/index/ancestor keys so
5787 // the predicate can reference them (save/restore rather
5788 // than blind unbind -- see bind_tuple_keys).
5789 let tuple_bindings = self.bind_tuple_keys(obj);
5790 let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
5791 let pred_res = self.evaluate_internal(pred, &at);
5792 tuple_bindings.restore(self);
5793 if self.is_truthy(&pred_res?) {
5794 kept.push(tup);
5795 }
5796 }
5797 tuples = kept;
5798 }
5799 Stage::Index(var) => {
5800 for (pos, tup) in tuples.iter_mut().enumerate() {
5801 if let JValue::Object(obj) = tup {
5802 let mut m = (**obj).clone();
5803 m.insert(format!("${}", var), JValue::from(pos as i64));
5804 *tup = JValue::object(m);
5805 }
5806 }
5807 }
5808 }
5809 }
5810 Ok(tuples)
5811 }
5812
5813 /// Helper to evaluate a complex path step
5814 fn evaluate_path_step(
5815 &mut self,
5816 step: &AstNode,
5817 current: &JValue,
5818 original_data: &JValue,
5819 ) -> Result<JValue, EvaluatorError> {
5820 // Special case: array mapping with object construction
5821 // e.g., items.{"name": name, "price": price}
5822 if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
5823 match (current, step) {
5824 (JValue::Array(arr), AstNode::Object(pairs)) => {
5825 // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
5826 if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
5827 let shape = arr.first().and_then(build_shape_cache);
5828 let mut mapped = Vec::with_capacity(arr.len());
5829 for item in arr.iter() {
5830 let result = if let Some(ref s) = shape {
5831 eval_compiled_shaped(
5832 &compiled,
5833 item,
5834 None,
5835 s,
5836 &self.options,
5837 self.start_time,
5838 )?
5839 } else {
5840 eval_compiled(
5841 &compiled,
5842 item,
5843 None,
5844 &self.options,
5845 self.start_time,
5846 )?
5847 };
5848 if !result.is_undefined() {
5849 mapped.push(result);
5850 }
5851 }
5852 return Ok(JValue::array(mapped));
5853 }
5854 // Fallback: full AST evaluation per element
5855 let mapped: Result<Vec<JValue>, EvaluatorError> = arr
5856 .iter()
5857 .map(|item| self.evaluate_internal(step, item))
5858 .collect();
5859 Ok(JValue::array(mapped?))
5860 }
5861 _ => unreachable!(),
5862 }
5863 } else {
5864 // Special case: array.$ should map $ over the array, returning each element
5865 // e.g., [1, 2, 3].$ returns [1, 2, 3]
5866 if let AstNode::Variable(name) = step {
5867 if name.is_empty() {
5868 // Bare $ - map over array if current is an array
5869 if let JValue::Array(arr) = current {
5870 // Map $ over each element - $ refers to each element in turn
5871 return Ok(JValue::Array(arr.clone()));
5872 } else {
5873 // For non-arrays, $ refers to the current value
5874 return Ok(current.clone());
5875 }
5876 }
5877 }
5878
5879 // Special case: Variable access on tuple arrays (from index binding #$var)
5880 // When current is a tuple array, we need to evaluate the variable against each tuple
5881 // so that tuple bindings ($i, etc.) can be found
5882 if matches!(step, AstNode::Variable(_)) {
5883 if let JValue::Array(arr) = current {
5884 // Check if this is a tuple array
5885 let is_tuple_array = arr.first().is_some_and(|first| {
5886 if let JValue::Object(obj) = first {
5887 obj.get("__tuple__") == Some(&JValue::Bool(true))
5888 } else {
5889 false
5890 }
5891 });
5892
5893 if is_tuple_array {
5894 // Map the variable lookup over each tuple
5895 let mut results = Vec::new();
5896 for tuple in arr.iter() {
5897 // Evaluate the variable in the context of this tuple
5898 // This allows tuple bindings ($i, etc.) to be found
5899 let val = self.evaluate_internal(step, tuple)?;
5900 if !val.is_null() && !val.is_undefined() {
5901 results.push(val);
5902 }
5903 }
5904 return Ok(JValue::array(results));
5905 }
5906 }
5907 }
5908
5909 // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
5910 // rather than being used to index/access the current value
5911 // e.g., items[price > 50] where [price > 50] is a filter operation
5912 // or $x.price where $x is a variable binding
5913 // or $$.field where $$ is the parent context
5914 // or [0..9] where it's an array constructor
5915 // or $^(field) where it's a sort operator
5916 // or (expr).field where (expr) is a block that evaluates to a value
5917 if matches!(
5918 step,
5919 AstNode::Binary { .. }
5920 | AstNode::Function { .. }
5921 | AstNode::Variable(_)
5922 | AstNode::ParentVariable(_)
5923 | AstNode::Parent(_)
5924 | AstNode::Array(_)
5925 | AstNode::Object(_)
5926 | AstNode::Sort { .. }
5927 | AstNode::Block(_)
5928 ) {
5929 // Evaluate the step in the context of original_data and return the result directly
5930 return self.evaluate_internal(step, original_data);
5931 }
5932
5933 // Standard path step evaluation for indexing/accessing current value
5934 let step_value = self.evaluate_internal(step, original_data)?;
5935 Ok(match (current, &step_value) {
5936 (JValue::Object(obj), JValue::String(key)) => {
5937 obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
5938 }
5939 (JValue::Array(arr), JValue::Number(n)) => {
5940 let index = *n as i64;
5941 let len = arr.len() as i64;
5942
5943 // Handle negative indexing (offset from end)
5944 let actual_idx = if index < 0 { len + index } else { index };
5945
5946 if actual_idx < 0 || actual_idx >= len {
5947 JValue::Undefined
5948 } else {
5949 arr[actual_idx as usize].clone()
5950 }
5951 }
5952 _ => JValue::Undefined,
5953 })
5954 }
5955 }
5956
5957 /// Evaluate a binary operation
5958 fn evaluate_binary_op(
5959 &mut self,
5960 op: crate::ast::BinaryOp,
5961 lhs: &AstNode,
5962 rhs: &AstNode,
5963 data: &JValue,
5964 ) -> Result<JValue, EvaluatorError> {
5965 use crate::ast::BinaryOp;
5966
5967 // Special handling for coalescing operator (??)
5968 // Returns right side if left is undefined (produces no value)
5969 // Note: literal null is a value, so it's NOT replaced
5970 if op == BinaryOp::Coalesce {
5971 // Try to evaluate the left side
5972 return match self.evaluate_internal(lhs, data) {
5973 Ok(value) => {
5974 // Successfully evaluated to a value (even if it's null)
5975 // Check if LHS is a literal null - keep it (null is a value, not undefined)
5976 if matches!(lhs, AstNode::Null) {
5977 Ok(value)
5978 }
5979 // For paths and variables, undefined (no match/unbound) - use RHS
5980 else if value.is_undefined()
5981 && (matches!(lhs, AstNode::Path { .. })
5982 || matches!(lhs, AstNode::String(_))
5983 || matches!(lhs, AstNode::Variable(_)))
5984 {
5985 self.evaluate_internal(rhs, data)
5986 } else {
5987 Ok(value)
5988 }
5989 }
5990 Err(_) => {
5991 // Evaluation failed (e.g., undefined variable) - use RHS
5992 self.evaluate_internal(rhs, data)
5993 }
5994 };
5995 }
5996
5997 // Special handling for default operator (?:)
5998 // Returns right side if left is falsy or a non-value (like a function)
5999 if op == BinaryOp::Default {
6000 let left = self.evaluate_internal(lhs, data)?;
6001 if self.is_truthy_for_default(&left) {
6002 return Ok(left);
6003 }
6004 return self.evaluate_internal(rhs, data);
6005 }
6006
6007 // Special handling for chain/pipe operator (~>)
6008 // Pipes the LHS result to the RHS function as the first argument
6009 // e.g., expr ~> func(arg2) becomes func(expr, arg2)
6010 if op == BinaryOp::ChainPipe {
6011 // Handle regex on RHS - treat as $match(lhs, regex)
6012 if let AstNode::Regex { pattern, flags } = rhs {
6013 // Evaluate LHS
6014 let lhs_value = self.evaluate_internal(lhs, data)?;
6015 // Do regex match inline
6016 return match lhs_value {
6017 JValue::String(s) => {
6018 // Build the regex
6019 let case_insensitive = flags.contains('i');
6020 let regex_pattern = if case_insensitive {
6021 format!("(?i){}", pattern)
6022 } else {
6023 pattern.clone()
6024 };
6025 match regex::Regex::new(®ex_pattern) {
6026 Ok(re) => {
6027 if let Some(m) = re.find(&s) {
6028 // Return match object
6029 let mut result = IndexMap::new();
6030 result.insert(
6031 "match".to_string(),
6032 JValue::string(m.as_str().to_string()),
6033 );
6034 result.insert(
6035 "start".to_string(),
6036 JValue::Number(m.start() as f64),
6037 );
6038 result
6039 .insert("end".to_string(), JValue::Number(m.end() as f64));
6040
6041 // Capture groups
6042 let mut groups = Vec::new();
6043 for cap in re.captures_iter(&s).take(1) {
6044 for i in 1..cap.len() {
6045 if let Some(c) = cap.get(i) {
6046 groups.push(JValue::string(c.as_str().to_string()));
6047 }
6048 }
6049 }
6050 if !groups.is_empty() {
6051 result.insert("groups".to_string(), JValue::array(groups));
6052 }
6053
6054 Ok(JValue::object(result))
6055 } else {
6056 Ok(JValue::Null)
6057 }
6058 }
6059 Err(e) => Err(EvaluatorError::EvaluationError(format!(
6060 "Invalid regex: {}",
6061 e
6062 ))),
6063 }
6064 }
6065 JValue::Null => Ok(JValue::Null),
6066 _ => Err(EvaluatorError::TypeError(
6067 "Left side of ~> /regex/ must be a string".to_string(),
6068 )),
6069 };
6070 }
6071
6072 // Early check: if LHS evaluates to undefined, return undefined
6073 // This matches JSONata behavior where undefined ~> anyFunc returns undefined
6074 let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
6075 if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
6076 return Ok(JValue::Undefined);
6077 }
6078
6079 // Handle different RHS types
6080 match rhs {
6081 AstNode::Function {
6082 name,
6083 args,
6084 is_builtin,
6085 } => {
6086 // RHS is a function call
6087 // Check if the function call has placeholder arguments (partial application)
6088 let has_placeholder =
6089 args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6090
6091 if has_placeholder {
6092 // Partial application: replace the first placeholder with LHS value
6093 let lhs_value = self.evaluate_internal(lhs, data)?;
6094 let mut filled_args = Vec::new();
6095 let mut lhs_used = false;
6096
6097 for arg in args.iter() {
6098 if matches!(arg, AstNode::Placeholder) && !lhs_used {
6099 // Replace first placeholder with evaluated LHS
6100 // We need to create a temporary binding to pass the value
6101 let temp_name = format!("__pipe_arg_{}", filled_args.len());
6102 self.context.bind(temp_name.clone(), lhs_value.clone());
6103 filled_args.push(AstNode::Variable(temp_name));
6104 lhs_used = true;
6105 } else {
6106 filled_args.push(arg.clone());
6107 }
6108 }
6109
6110 // Evaluate the function with filled args
6111 let result =
6112 self.evaluate_function_call(name, &filled_args, *is_builtin, data);
6113
6114 // Clean up temp bindings
6115 for (i, arg) in args.iter().enumerate() {
6116 if matches!(arg, AstNode::Placeholder) {
6117 self.context.unbind(&format!("__pipe_arg_{}", i));
6118 }
6119 }
6120
6121 // Unwrap singleton results from chain operator
6122 return result.map(|v| self.unwrap_singleton(v));
6123 } else {
6124 // No placeholders: build args list with LHS as first argument
6125 let mut all_args = vec![lhs.clone()];
6126 all_args.extend_from_slice(args);
6127 // Unwrap singleton results from chain operator
6128 return self
6129 .evaluate_function_call(name, &all_args, *is_builtin, data)
6130 .map(|v| self.unwrap_singleton(v));
6131 }
6132 }
6133 AstNode::Variable(var_name) => {
6134 // RHS is a function reference (no parens)
6135 // e.g., $average($tempReadings) ~> $round
6136 let all_args = vec![lhs.clone()];
6137 // Unwrap singleton results from chain operator
6138 return self
6139 .evaluate_function_call(var_name, &all_args, true, data)
6140 .map(|v| self.unwrap_singleton(v));
6141 }
6142 AstNode::Binary {
6143 op: BinaryOp::ChainPipe,
6144 ..
6145 } => {
6146 // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
6147 // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
6148 let lhs_value = self.evaluate_internal(lhs, data)?;
6149 return self.evaluate_internal(rhs, &lhs_value);
6150 }
6151 AstNode::Transform { .. } => {
6152 // RHS is a transform - invoke it with LHS as input
6153 // Evaluate LHS first
6154 let lhs_value = self.evaluate_internal(lhs, data)?;
6155
6156 // Bind $ to the LHS value, then evaluate the transform
6157 let saved_binding = self.context.lookup("$").cloned();
6158 self.context.bind("$".to_string(), lhs_value.clone());
6159
6160 let result = self.evaluate_internal(rhs, data);
6161
6162 // Restore $ binding
6163 if let Some(saved) = saved_binding {
6164 self.context.bind("$".to_string(), saved);
6165 } else {
6166 self.context.unbind("$");
6167 }
6168
6169 // Unwrap singleton results from chain operator
6170 return result.map(|v| self.unwrap_singleton(v));
6171 }
6172 AstNode::Lambda {
6173 params,
6174 body,
6175 signature,
6176 thunk,
6177 } => {
6178 // RHS is a lambda - invoke it with LHS as argument
6179 let lhs_value = self.evaluate_internal(lhs, data)?;
6180 // Unwrap singleton results from chain operator
6181 return self
6182 .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
6183 .map(|v| self.unwrap_singleton(v));
6184 }
6185 AstNode::Path { steps } => {
6186 // RHS is a path expression (e.g., function call with predicate: $map($f)[])
6187 // If the first step is a function call, we need to add LHS as first argument
6188 if let Some(first_step) = steps.first() {
6189 match &first_step.node {
6190 AstNode::Function {
6191 name,
6192 args,
6193 is_builtin,
6194 } => {
6195 // Prepend LHS to the function arguments
6196 let mut all_args = vec![lhs.clone()];
6197 all_args.extend_from_slice(args);
6198
6199 // Call the function
6200 let mut result = self.evaluate_function_call(
6201 name,
6202 &all_args,
6203 *is_builtin,
6204 data,
6205 )?;
6206
6207 // Apply stages from the first step (e.g., predicates)
6208 for stage in &first_step.stages {
6209 match stage {
6210 Stage::Filter(filter_expr) => {
6211 result = self.evaluate_predicate_as_stage(
6212 &result,
6213 filter_expr,
6214 )?;
6215 }
6216 Stage::Index(_) => {}
6217 }
6218 }
6219
6220 // Apply remaining path steps if any
6221 if steps.len() > 1 {
6222 let remaining_path = AstNode::Path {
6223 steps: steps[1..].to_vec(),
6224 };
6225 result = self.evaluate_internal(&remaining_path, &result)?;
6226 }
6227
6228 // Unwrap singleton results from chain operator, unless there are stages
6229 // Stages (like predicates) indicate we want to preserve array structure
6230 if !first_step.stages.is_empty() || steps.len() > 1 {
6231 return Ok(result);
6232 } else {
6233 return Ok(self.unwrap_singleton(result));
6234 }
6235 }
6236 AstNode::Variable(var_name) => {
6237 // Variable that should resolve to a function
6238 let all_args = vec![lhs.clone()];
6239 let mut result =
6240 self.evaluate_function_call(var_name, &all_args, true, data)?;
6241
6242 // Apply stages from the first step
6243 for stage in &first_step.stages {
6244 match stage {
6245 Stage::Filter(filter_expr) => {
6246 result = self.evaluate_predicate_as_stage(
6247 &result,
6248 filter_expr,
6249 )?;
6250 }
6251 Stage::Index(_) => {}
6252 }
6253 }
6254
6255 // Apply remaining path steps if any
6256 if steps.len() > 1 {
6257 let remaining_path = AstNode::Path {
6258 steps: steps[1..].to_vec(),
6259 };
6260 result = self.evaluate_internal(&remaining_path, &result)?;
6261 }
6262
6263 // Unwrap singleton results from chain operator, unless there are stages
6264 // Stages (like predicates) indicate we want to preserve array structure
6265 if !first_step.stages.is_empty() || steps.len() > 1 {
6266 return Ok(result);
6267 } else {
6268 return Ok(self.unwrap_singleton(result));
6269 }
6270 }
6271 _ => {
6272 // Other path types - just evaluate normally with LHS as context
6273 let lhs_value = self.evaluate_internal(lhs, data)?;
6274 return self
6275 .evaluate_internal(rhs, &lhs_value)
6276 .map(|v| self.unwrap_singleton(v));
6277 }
6278 }
6279 }
6280
6281 // Empty path? Shouldn't happen, but handle it
6282 let lhs_value = self.evaluate_internal(lhs, data)?;
6283 return self
6284 .evaluate_internal(rhs, &lhs_value)
6285 .map(|v| self.unwrap_singleton(v));
6286 }
6287 _ => {
6288 return Err(EvaluatorError::TypeError(
6289 "Right side of ~> must be a function call or function reference"
6290 .to_string(),
6291 ));
6292 }
6293 }
6294 }
6295
6296 // Special handling for variable binding (:=)
6297 if op == BinaryOp::ColonEqual {
6298 // Extract variable name from LHS
6299 let var_name = match lhs {
6300 AstNode::Variable(name) => name.clone(),
6301 _ => {
6302 return Err(EvaluatorError::TypeError(
6303 "Left side of := must be a variable".to_string(),
6304 ))
6305 }
6306 };
6307
6308 // Check if RHS is a lambda - store it specially
6309 if let AstNode::Lambda {
6310 params,
6311 body,
6312 signature,
6313 thunk,
6314 } = rhs
6315 {
6316 // Store the lambda AST for later invocation
6317 // Capture only the free variables referenced by the lambda body
6318 let captured_env = self.capture_environment_for(body, params);
6319 let compiled_body = if !thunk {
6320 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
6321 try_compile_expr_with_allowed_vars(body, &var_refs)
6322 } else {
6323 None
6324 };
6325 let stored_lambda = StoredLambda {
6326 params: params.clone(),
6327 body: (**body).clone(),
6328 compiled_body,
6329 signature: signature.clone(),
6330 captured_env,
6331 captured_data: Some(data.clone()),
6332 thunk: *thunk,
6333 };
6334 let lambda_params = stored_lambda.params.clone();
6335 let lambda_sig = stored_lambda.signature.clone();
6336 self.context.bind_lambda(var_name.clone(), stored_lambda);
6337
6338 // Return a lambda marker value (include _lambda_id so it can be found later)
6339 let lambda_repr = JValue::lambda(
6340 var_name.as_str(),
6341 lambda_params,
6342 Some(var_name.clone()),
6343 lambda_sig,
6344 );
6345 return Ok(lambda_repr);
6346 }
6347
6348 // Check if RHS is a pure function composition (ChainPipe between function references)
6349 // e.g., $uppertrim := $trim ~> $uppercase
6350 // This creates a lambda that composes the functions.
6351 // But NOT for data ~> function, which should be evaluated immediately.
6352 // e.g., $result := data ~> $map($fn) should evaluate the pipe
6353 if let AstNode::Binary {
6354 op: BinaryOp::ChainPipe,
6355 lhs: chain_lhs,
6356 rhs: chain_rhs,
6357 } = rhs
6358 {
6359 // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
6360 // If LHS is data (array, object, function call result, etc.), evaluate the pipe
6361 let is_function_composition = match chain_lhs.as_ref() {
6362 // LHS is a function reference like $trim or $sum
6363 AstNode::Variable(name)
6364 if self.is_builtin_function(name)
6365 || self.context.lookup_lambda(name).is_some() =>
6366 {
6367 true
6368 }
6369 // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
6370 AstNode::Binary {
6371 op: BinaryOp::ChainPipe,
6372 ..
6373 } => true,
6374 // A function call with placeholder creates a partial application
6375 // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
6376 AstNode::Function { args, .. }
6377 if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
6378 {
6379 true
6380 }
6381 // Anything else (data, function calls, arrays, etc.) is not pure composition
6382 _ => false,
6383 };
6384
6385 if is_function_composition {
6386 // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
6387 // The original chain is $trim ~> $uppercase (left-associative)
6388 // We want to create: ($ ~> $trim) ~> $uppercase
6389 let param_name = "$".to_string();
6390
6391 // First create $ ~> $trim
6392 let first_pipe = AstNode::Binary {
6393 op: BinaryOp::ChainPipe,
6394 lhs: Box::new(AstNode::Variable(param_name.clone())),
6395 rhs: chain_lhs.clone(),
6396 };
6397
6398 // Then wrap with ~> $uppercase (or the rest of the chain)
6399 let composed_body = AstNode::Binary {
6400 op: BinaryOp::ChainPipe,
6401 lhs: Box::new(first_pipe),
6402 rhs: chain_rhs.clone(),
6403 };
6404
6405 let stored_lambda = StoredLambda {
6406 params: vec![param_name],
6407 body: composed_body,
6408 compiled_body: None, // ChainPipe body is not compilable
6409 signature: None,
6410 captured_env: self.capture_current_environment(),
6411 captured_data: Some(data.clone()),
6412 thunk: false,
6413 };
6414 self.context.bind_lambda(var_name.clone(), stored_lambda);
6415
6416 // Return a lambda marker value (include _lambda_id for later lookup)
6417 let lambda_repr = JValue::lambda(
6418 var_name.as_str(),
6419 vec!["$".to_string()],
6420 Some(var_name.clone()),
6421 None::<String>,
6422 );
6423 return Ok(lambda_repr);
6424 }
6425 // If not function composition, fall through to normal evaluation below
6426 }
6427
6428 // Evaluate the RHS
6429 let value = self.evaluate_internal(rhs, data)?;
6430
6431 // If the value is a lambda, copy the stored lambda to the new variable name
6432 if let Some(stored) = self.lookup_lambda_from_value(&value) {
6433 self.context.bind_lambda(var_name.clone(), stored);
6434 }
6435
6436 // Bind even if undefined (null) so inner scopes can shadow outer variables
6437 self.context.bind(var_name, value.clone());
6438 return Ok(value);
6439 }
6440
6441 // Special handling for 'In' operator - check for array filtering
6442 // Must evaluate lhs first to determine if this is array filtering
6443 if op == BinaryOp::In {
6444 let left = self.evaluate_internal(lhs, data)?;
6445
6446 // Check if this is array filtering: array[predicate]
6447 if matches!(left, JValue::Array(_)) {
6448 // Try evaluating rhs in current context to see if it's a simple index
6449 let right_result = self.evaluate_internal(rhs, data);
6450
6451 if let Ok(JValue::Number(_)) = right_result {
6452 // Simple numeric index: array[n]
6453 return self.array_index(&left, &right_result.unwrap());
6454 } else {
6455 // This is array filtering: array[predicate]
6456 // Evaluate the predicate for each array item
6457 return self.array_filter(lhs, rhs, &left, data);
6458 }
6459 }
6460 }
6461
6462 // Special handling for logical operators (short-circuit evaluation)
6463 if op == BinaryOp::And {
6464 let left = self.evaluate_internal(lhs, data)?;
6465 if !self.is_truthy(&left) {
6466 // Short-circuit: if left is falsy, return false without evaluating right
6467 return Ok(JValue::Bool(false));
6468 }
6469 let right = self.evaluate_internal(rhs, data)?;
6470 return Ok(JValue::Bool(self.is_truthy(&right)));
6471 }
6472
6473 if op == BinaryOp::Or {
6474 let left = self.evaluate_internal(lhs, data)?;
6475 if self.is_truthy(&left) {
6476 // Short-circuit: if left is truthy, return true without evaluating right
6477 return Ok(JValue::Bool(true));
6478 }
6479 let right = self.evaluate_internal(rhs, data)?;
6480 return Ok(JValue::Bool(self.is_truthy(&right)));
6481 }
6482
6483 // Check if operands are explicit null literals (vs undefined from variables)
6484 let left_is_explicit_null = matches!(lhs, AstNode::Null);
6485 let right_is_explicit_null = matches!(rhs, AstNode::Null);
6486
6487 // Standard evaluation: evaluate both operands
6488 let left = self.evaluate_internal(lhs, data)?;
6489 let right = self.evaluate_internal(rhs, data)?;
6490
6491 match op {
6492 BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6493 BinaryOp::Subtract => {
6494 self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6495 }
6496 BinaryOp::Multiply => {
6497 self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6498 }
6499 BinaryOp::Divide => {
6500 self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6501 }
6502 BinaryOp::Modulo => {
6503 self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6504 }
6505
6506 BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
6507 BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
6508 BinaryOp::LessThan => {
6509 self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6510 }
6511 BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6512 &left,
6513 &right,
6514 left_is_explicit_null,
6515 right_is_explicit_null,
6516 ),
6517 BinaryOp::GreaterThan => {
6518 self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6519 }
6520 BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6521 &left,
6522 &right,
6523 left_is_explicit_null,
6524 right_is_explicit_null,
6525 ),
6526
6527 // And/Or handled above with short-circuit evaluation
6528 BinaryOp::And | BinaryOp::Or => unreachable!(),
6529
6530 BinaryOp::Concatenate => self.concatenate(&left, &right),
6531 BinaryOp::Range => self.range(&left, &right),
6532 BinaryOp::In => self.in_operator(&left, &right),
6533
6534 // Focus binding: should be resolved by ast_transform pass (Task 2)
6535 BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6536 "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6537 )),
6538
6539 // Index binding: should be resolved by ast_transform pass (Task 4,
6540 // which retired the dedicated AstNode::IndexBind variant in favor
6541 // of this generic Binary marker, mirroring FocusBind above)
6542 BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6543 "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6544 )),
6545
6546 // These operators are all handled as special cases earlier in evaluate_binary_op
6547 BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6548 unreachable!()
6549 }
6550 }
6551 }
6552
6553 /// Evaluate a unary operation
6554 fn evaluate_unary_op(
6555 &mut self,
6556 op: crate::ast::UnaryOp,
6557 operand: &AstNode,
6558 data: &JValue,
6559 ) -> Result<JValue, EvaluatorError> {
6560 use crate::ast::UnaryOp;
6561
6562 let value = self.evaluate_internal(operand, data)?;
6563
6564 match op {
6565 UnaryOp::Negate => match value {
6566 // undefined returns undefined
6567 JValue::Null | JValue::Undefined => Ok(JValue::Null),
6568 JValue::Number(n) => Ok(JValue::Number(-n)),
6569 _ => Err(EvaluatorError::TypeError(
6570 "D1002: Cannot negate non-number value".to_string(),
6571 )),
6572 },
6573 UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6574 }
6575 }
6576
6577 /// Try to fuse an aggregate function call with its Path argument.
6578 /// Handles patterns like:
6579 /// - $sum(arr.field) → iterate arr, extract field, accumulate
6580 /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6581 ///
6582 /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6583 fn try_fused_aggregate(
6584 &mut self,
6585 name: &str,
6586 arg: &AstNode,
6587 data: &JValue,
6588 ) -> Result<Option<JValue>, EvaluatorError> {
6589 // Only applies to numeric aggregates
6590 if !matches!(name, "sum" | "max" | "min" | "average") {
6591 return Ok(None);
6592 }
6593
6594 // Argument must be a Path
6595 let AstNode::Path { steps } = arg else {
6596 return Ok(None);
6597 };
6598
6599 // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6600 // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6601 if steps.len() != 2 {
6602 return Ok(None);
6603 }
6604
6605 // Last step must be a simple Name (the field to extract)
6606 let field_step = &steps[1];
6607 if !field_step.stages.is_empty() {
6608 return Ok(None);
6609 }
6610 let AstNode::Name(extract_field) = &field_step.node else {
6611 return Ok(None);
6612 };
6613
6614 // First step: Name with optional filter stage
6615 let arr_step = &steps[0];
6616 let AstNode::Name(arr_name) = &arr_step.node else {
6617 return Ok(None);
6618 };
6619
6620 // Get the source array from data
6621 let arr = match data {
6622 JValue::Object(obj) => match obj.get(arr_name) {
6623 Some(JValue::Array(arr)) => arr,
6624 _ => return Ok(None),
6625 },
6626 _ => return Ok(None),
6627 };
6628
6629 // Check for filter stage — try CompiledExpr for the predicate
6630 let filter_compiled = match arr_step.stages.as_slice() {
6631 [] => None,
6632 [Stage::Filter(pred)] => try_compile_expr(pred),
6633 _ => return Ok(None),
6634 };
6635 // If filter stage exists but wasn't compilable, bail out
6636 if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6637 return Ok(None);
6638 }
6639
6640 // Build shape cache for the array
6641 let shape = arr.first().and_then(build_shape_cache);
6642
6643 // Fused iteration: filter (optional) + extract + aggregate
6644 let mut total = 0.0f64;
6645 let mut count = 0usize;
6646 let mut max_val = f64::NEG_INFINITY;
6647 let mut min_val = f64::INFINITY;
6648 let mut has_any = false;
6649
6650 for item in arr.iter() {
6651 // Apply compiled filter if present
6652 if let Some(ref compiled) = filter_compiled {
6653 let result = if let Some(ref s) = shape {
6654 eval_compiled_shaped(compiled, item, None, s, &self.options, self.start_time)?
6655 } else {
6656 eval_compiled(compiled, item, None, &self.options, self.start_time)?
6657 };
6658 if !compiled_is_truthy(&result) {
6659 continue;
6660 }
6661 }
6662
6663 // Extract field value
6664 let val = match item {
6665 JValue::Object(obj) => match obj.get(extract_field) {
6666 Some(JValue::Number(n)) => *n,
6667 Some(_) | None => continue, // Skip non-numeric / missing
6668 },
6669 _ => continue,
6670 };
6671
6672 has_any = true;
6673 match name {
6674 "sum" => total += val,
6675 "max" => max_val = max_val.max(val),
6676 "min" => min_val = min_val.min(val),
6677 "average" => {
6678 total += val;
6679 count += 1;
6680 }
6681 _ => unreachable!(),
6682 }
6683 }
6684
6685 if !has_any {
6686 return Ok(Some(match name {
6687 "sum" => JValue::from(0i64),
6688 "average" | "max" | "min" => JValue::Null,
6689 _ => unreachable!(),
6690 }));
6691 }
6692
6693 Ok(Some(match name {
6694 "sum" => JValue::Number(total),
6695 "max" => JValue::Number(max_val),
6696 "min" => JValue::Number(min_val),
6697 "average" => JValue::Number(total / count as f64),
6698 _ => unreachable!(),
6699 }))
6700 }
6701
6702 /// Evaluate a function call
6703 fn evaluate_function_call(
6704 &mut self,
6705 name: &str,
6706 args: &[AstNode],
6707 is_builtin: bool,
6708 data: &JValue,
6709 ) -> Result<JValue, EvaluatorError> {
6710 use crate::functions;
6711
6712 // Check for partial application (any argument is a Placeholder)
6713 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6714 if has_placeholder {
6715 return self.create_partial_application(name, args, is_builtin, data);
6716 }
6717
6718 // FIRST check if this variable holds a function value (lambda or builtin reference)
6719 // This is critical for:
6720 // 1. Allowing function parameters to shadow stored lambdas
6721 // (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
6722 // 2. Calling built-in functions passed as parameters
6723 // (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
6724 if let Some(value) = self.context.lookup(name).cloned() {
6725 if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
6726 let mut evaluated_args = Vec::with_capacity(args.len());
6727 for arg in args {
6728 evaluated_args.push(self.evaluate_internal(arg, data)?);
6729 }
6730 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6731 }
6732 if let JValue::Builtin { name: builtin_name } = &value {
6733 // This is a built-in function reference (e.g., $f bound to $sum)
6734 let mut evaluated_args = Vec::with_capacity(args.len());
6735 for arg in args {
6736 evaluated_args.push(self.evaluate_internal(arg, data)?);
6737 }
6738 return self.call_builtin_with_values(builtin_name, &evaluated_args);
6739 }
6740 }
6741
6742 // THEN check if this is a stored lambda (user-defined function by name)
6743 // This only applies if not shadowed by a binding above
6744 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
6745 let mut evaluated_args = Vec::with_capacity(args.len());
6746 for arg in args {
6747 evaluated_args.push(self.evaluate_internal(arg, data)?);
6748 }
6749 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6750 }
6751
6752 // If the function was called without $ prefix and it's not a stored lambda,
6753 // it's an error (unknown function without $ prefix)
6754 if !is_builtin && name != "__lambda__" {
6755 return Err(EvaluatorError::ReferenceError(format!(
6756 "Unknown function: {}",
6757 name
6758 )));
6759 }
6760
6761 // Special handling for $exists function
6762 // It needs to know if the argument is explicit null vs undefined
6763 if name == "exists" && args.len() == 1 {
6764 let arg = &args[0];
6765
6766 // Check if it's an explicit null literal
6767 if matches!(arg, AstNode::Null) {
6768 return Ok(JValue::Bool(true)); // Explicit null exists
6769 }
6770
6771 // Check if it's a function reference
6772 if let AstNode::Variable(var_name) = arg {
6773 if self.is_builtin_function(var_name) {
6774 return Ok(JValue::Bool(true)); // Built-in function exists
6775 }
6776
6777 // Check if it's a stored lambda
6778 if self.context.lookup_lambda(var_name).is_some() {
6779 return Ok(JValue::Bool(true)); // Lambda exists
6780 }
6781
6782 // Check if the variable is defined
6783 if let Some(val) = self.context.lookup(var_name) {
6784 // A variable bound to the undefined marker doesn't "exist"
6785 if val.is_undefined() {
6786 return Ok(JValue::Bool(false));
6787 }
6788 return Ok(JValue::Bool(true)); // Variable is defined (even if null)
6789 } else {
6790 return Ok(JValue::Bool(false)); // Variable is undefined
6791 }
6792 }
6793
6794 // For other expressions, evaluate and check if non-null/non-undefined
6795 let value = self.evaluate_internal(arg, data)?;
6796 return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
6797 }
6798
6799 // Check if any arguments are undefined variables or undefined paths
6800 // Functions like $not() should return undefined when given undefined values
6801 for arg in args {
6802 // Check for undefined variable (e.g., $undefined_var)
6803 if let AstNode::Variable(var_name) = arg {
6804 // Skip built-in function names - they're function references, not undefined variables
6805 if !var_name.is_empty()
6806 && !self.is_builtin_function(var_name)
6807 && self.context.lookup(var_name).is_none()
6808 {
6809 // Undefined variable - for functions that should propagate undefined
6810 if propagates_undefined(name) {
6811 return Ok(JValue::Null); // Return undefined
6812 }
6813 }
6814 }
6815 // Check for simple field name (e.g., blah) that evaluates to undefined
6816 if let AstNode::Name(field_name) = arg {
6817 let field_exists =
6818 matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
6819 if !field_exists && propagates_undefined(name) {
6820 return Ok(JValue::Null);
6821 }
6822 }
6823 // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
6824 // Field accesses are represented as AstNode::Path. String literals should never
6825 // be checked for undefined propagation.
6826 // Check for Path expressions that evaluate to undefined
6827 if let AstNode::Path { steps } = arg {
6828 // For paths that evaluate to null, we need to determine if it's because:
6829 // 1. A field doesn't exist (undefined) - should propagate as undefined
6830 // 2. A field exists with value null - should throw T0410
6831 //
6832 // We can distinguish these by checking if the path is accessing a field
6833 // that doesn't exist on an object vs one that has an explicit null value.
6834 if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
6835 // Path evaluated to null - now check if it's truly undefined
6836 // For single-step paths, check if the field exists
6837 if steps.len() == 1 {
6838 // Get field name - could be Name (identifier) or String (quoted)
6839 let field_name = match &steps[0].node {
6840 AstNode::Name(n) => Some(n.as_str()),
6841 AstNode::String(s) => Some(s.as_str()),
6842 _ => None,
6843 };
6844 if let Some(field) = field_name {
6845 match data {
6846 JValue::Object(obj) => {
6847 if !obj.contains_key(field) {
6848 // Field doesn't exist - return undefined
6849 if propagates_undefined(name) {
6850 return Ok(JValue::Null);
6851 }
6852 }
6853 // Field exists with value null - continue to throw T0410
6854 }
6855 // Trying to access field on null data - return undefined
6856 JValue::Null if propagates_undefined(name) => {
6857 return Ok(JValue::Null);
6858 }
6859 _ => {}
6860 }
6861 }
6862 }
6863 // For multi-step paths, check if any intermediate step failed
6864 else if steps.len() > 1 {
6865 // Evaluate each step to find where it breaks
6866 let mut current = data;
6867 let mut failed_due_to_missing_field = false;
6868
6869 for (i, step) in steps.iter().enumerate() {
6870 if let AstNode::Name(field_name) = &step.node {
6871 match current {
6872 JValue::Object(obj) => {
6873 if let Some(val) = obj.get(field_name) {
6874 current = val;
6875 } else {
6876 // Field doesn't exist
6877 failed_due_to_missing_field = true;
6878 break;
6879 }
6880 }
6881 JValue::Array(_) => {
6882 // Array access - evaluate normally
6883 break;
6884 }
6885 JValue::Null => {
6886 // Hit null in the middle of the path
6887 if i > 0 {
6888 // Previous field had null value - not undefined
6889 failed_due_to_missing_field = false;
6890 }
6891 break;
6892 }
6893 _ => break,
6894 }
6895 }
6896 }
6897
6898 if failed_due_to_missing_field && propagates_undefined(name) {
6899 return Ok(JValue::Null);
6900 }
6901 }
6902 }
6903 }
6904 }
6905
6906 // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
6907 // try to fuse filter+extract+aggregate into a single pass.
6908 if args.len() == 1 {
6909 if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
6910 return Ok(result);
6911 }
6912 }
6913
6914 let mut evaluated_args = Vec::with_capacity(args.len());
6915 for arg in args {
6916 evaluated_args.push(self.evaluate_internal(arg, data)?);
6917 }
6918
6919 // JSONata feature: when a function is called with no arguments but expects
6920 // at least one, use the current context value (data) as the implicit first argument
6921 // This also applies when functions expecting N arguments receive N-1 arguments,
6922 // in which case the context value becomes the first argument
6923 let context_functions_zero_arg = [
6924 "string",
6925 "number",
6926 "boolean",
6927 "uppercase",
6928 "lowercase",
6929 "fromMillis",
6930 ];
6931 let context_functions_missing_first = [
6932 "substringBefore",
6933 "substringAfter",
6934 "contains",
6935 "split",
6936 "replace",
6937 ];
6938
6939 if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
6940 // Use the current context value as the implicit argument
6941 evaluated_args.push(data.clone());
6942 } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
6943 // These functions expect 2+ arguments, but received 1
6944 // Only insert context if it's a compatible type (string for string functions)
6945 // Otherwise, let the function throw T0411 for wrong argument count
6946 if matches!(data, JValue::String(_)) {
6947 evaluated_args.insert(0, data.clone());
6948 }
6949 }
6950
6951 // Special handling for $string() with no explicit arguments
6952 // After context insertion, check if the argument is null (undefined context)
6953 if name == "string"
6954 && args.is_empty()
6955 && !evaluated_args.is_empty()
6956 && evaluated_args[0].is_null()
6957 {
6958 // Context was null/undefined, so return undefined
6959 return Ok(JValue::Null);
6960 }
6961
6962 match name {
6963 "string" => {
6964 if evaluated_args.len() > 2 {
6965 return Err(EvaluatorError::EvaluationError(
6966 "string() takes at most 2 arguments".to_string(),
6967 ));
6968 }
6969
6970 let prettify = if evaluated_args.len() == 2 {
6971 match &evaluated_args[1] {
6972 JValue::Bool(b) => Some(*b),
6973 _ => {
6974 return Err(EvaluatorError::TypeError(
6975 "string() prettify parameter must be a boolean".to_string(),
6976 ))
6977 }
6978 }
6979 } else {
6980 None
6981 };
6982
6983 Ok(functions::string::string(&evaluated_args[0], prettify)?)
6984 }
6985 "length" => {
6986 if evaluated_args.len() != 1 {
6987 return Err(EvaluatorError::EvaluationError(
6988 "length() requires exactly 1 argument".to_string(),
6989 ));
6990 }
6991 match &evaluated_args[0] {
6992 JValue::String(s) => Ok(functions::string::length(s)?),
6993 _ => Err(EvaluatorError::TypeError(
6994 "T0410: Argument 1 of function length does not match function signature"
6995 .to_string(),
6996 )),
6997 }
6998 }
6999 "uppercase" => {
7000 if evaluated_args.len() != 1 {
7001 return Err(EvaluatorError::EvaluationError(
7002 "uppercase() requires exactly 1 argument".to_string(),
7003 ));
7004 }
7005 if evaluated_args[0].is_undefined() {
7006 return Ok(JValue::Undefined);
7007 }
7008 match &evaluated_args[0] {
7009 JValue::String(s) => Ok(functions::string::uppercase(s)?),
7010 _ => Err(EvaluatorError::TypeError(
7011 "T0410: Argument 1 of function uppercase does not match function signature"
7012 .to_string(),
7013 )),
7014 }
7015 }
7016 "lowercase" => {
7017 if evaluated_args.len() != 1 {
7018 return Err(EvaluatorError::EvaluationError(
7019 "lowercase() requires exactly 1 argument".to_string(),
7020 ));
7021 }
7022 if evaluated_args[0].is_undefined() {
7023 return Ok(JValue::Undefined);
7024 }
7025 match &evaluated_args[0] {
7026 JValue::String(s) => Ok(functions::string::lowercase(s)?),
7027 _ => Err(EvaluatorError::TypeError(
7028 "T0410: Argument 1 of function lowercase does not match function signature"
7029 .to_string(),
7030 )),
7031 }
7032 }
7033 "number" => {
7034 if evaluated_args.is_empty() {
7035 return Err(EvaluatorError::EvaluationError(
7036 "number() requires at least 1 argument".to_string(),
7037 ));
7038 }
7039 if evaluated_args.len() > 1 {
7040 return Err(EvaluatorError::TypeError(
7041 "T0410: Argument 2 of function number does not match function signature"
7042 .to_string(),
7043 ));
7044 }
7045 if evaluated_args[0].is_undefined() {
7046 return Ok(JValue::Undefined);
7047 }
7048 Ok(functions::numeric::number(&evaluated_args[0])?)
7049 }
7050 "sum" => {
7051 if evaluated_args.len() != 1 {
7052 return Err(EvaluatorError::EvaluationError(
7053 "sum() requires exactly 1 argument".to_string(),
7054 ));
7055 }
7056 // Return undefined if argument is undefined
7057 if evaluated_args[0].is_undefined() {
7058 return Ok(JValue::Undefined);
7059 }
7060 match &evaluated_args[0] {
7061 JValue::Null => Ok(JValue::Null),
7062 JValue::Array(arr) => {
7063 // Use zero-clone iterator-based sum
7064 Ok(aggregation::sum(arr)?)
7065 }
7066 // Non-array values: extract number directly
7067 JValue::Number(n) => Ok(JValue::Number(*n)),
7068 other => Ok(functions::numeric::sum(&[other.clone()])?),
7069 }
7070 }
7071 "count" => {
7072 if evaluated_args.len() != 1 {
7073 return Err(EvaluatorError::EvaluationError(
7074 "count() requires exactly 1 argument".to_string(),
7075 ));
7076 }
7077 // Return 0 if argument is undefined
7078 if evaluated_args[0].is_undefined() {
7079 return Ok(JValue::from(0i64));
7080 }
7081 match &evaluated_args[0] {
7082 JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
7083 JValue::Array(arr) => Ok(functions::array::count(arr)?),
7084 _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
7085 }
7086 }
7087 "substring" => {
7088 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7089 return Err(EvaluatorError::EvaluationError(
7090 "substring() requires 2 or 3 arguments".to_string(),
7091 ));
7092 }
7093 if evaluated_args[0].is_undefined() {
7094 return Ok(JValue::Undefined);
7095 }
7096 match (&evaluated_args[0], &evaluated_args[1]) {
7097 (JValue::String(s), JValue::Number(start)) => {
7098 let length = if evaluated_args.len() == 3 {
7099 match &evaluated_args[2] {
7100 JValue::Number(l) => Some(*l as i64),
7101 _ => return Err(EvaluatorError::TypeError(
7102 "T0410: Argument 3 of function substring does not match function signature".to_string(),
7103 )),
7104 }
7105 } else {
7106 None
7107 };
7108 Ok(functions::string::substring(s, *start as i64, length)?)
7109 }
7110 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7111 "T0410: Argument 2 of function substring does not match function signature"
7112 .to_string(),
7113 )),
7114 _ => Err(EvaluatorError::TypeError(
7115 "T0410: Argument 1 of function substring does not match function signature"
7116 .to_string(),
7117 )),
7118 }
7119 }
7120 "substringBefore" => {
7121 if evaluated_args.len() != 2 {
7122 return Err(EvaluatorError::TypeError(
7123 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
7124 ));
7125 }
7126 if evaluated_args[0].is_undefined() {
7127 return Ok(JValue::Undefined);
7128 }
7129 match (&evaluated_args[0], &evaluated_args[1]) {
7130 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
7131 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7132 "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
7133 )),
7134 _ => Err(EvaluatorError::TypeError(
7135 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
7136 )),
7137 }
7138 }
7139 "substringAfter" => {
7140 if evaluated_args.len() != 2 {
7141 return Err(EvaluatorError::TypeError(
7142 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
7143 ));
7144 }
7145 if evaluated_args[0].is_undefined() {
7146 return Ok(JValue::Undefined);
7147 }
7148 match (&evaluated_args[0], &evaluated_args[1]) {
7149 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
7150 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7151 "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
7152 )),
7153 _ => Err(EvaluatorError::TypeError(
7154 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
7155 )),
7156 }
7157 }
7158 "pad" => {
7159 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7160 return Err(EvaluatorError::EvaluationError(
7161 "pad() requires 2 or 3 arguments".to_string(),
7162 ));
7163 }
7164
7165 // First argument: string to pad
7166 let string = match &evaluated_args[0] {
7167 JValue::String(s) => s.clone(),
7168 JValue::Null => return Ok(JValue::Null),
7169 JValue::Undefined => return Ok(JValue::Undefined),
7170 _ => {
7171 return Err(EvaluatorError::TypeError(
7172 "pad() first argument must be a string".to_string(),
7173 ))
7174 }
7175 };
7176
7177 // Second argument: width (negative = left pad, positive = right pad)
7178 let width = match &evaluated_args.get(1) {
7179 Some(JValue::Number(n)) => *n as i32,
7180 _ => {
7181 return Err(EvaluatorError::TypeError(
7182 "pad() second argument must be a number".to_string(),
7183 ))
7184 }
7185 };
7186
7187 // Third argument: padding string (optional, defaults to space)
7188 let pad_string = match evaluated_args.get(2) {
7189 Some(JValue::String(s)) if !s.is_empty() => s.clone(),
7190 _ => Rc::from(" "),
7191 };
7192
7193 let abs_width = width.unsigned_abs() as usize;
7194 // Count Unicode characters (code points), not bytes
7195 let char_count = string.chars().count();
7196
7197 if char_count >= abs_width {
7198 // String is already long enough
7199 return Ok(JValue::string(string));
7200 }
7201
7202 let padding_needed = abs_width - char_count;
7203
7204 let pad_chars: Vec<char> = pad_string.chars().collect();
7205 let mut padding = String::with_capacity(padding_needed);
7206 for i in 0..padding_needed {
7207 padding.push(pad_chars[i % pad_chars.len()]);
7208 }
7209
7210 let result = if width < 0 {
7211 // Left pad (negative width)
7212 format!("{}{}", padding, string)
7213 } else {
7214 // Right pad (positive width)
7215 format!("{}{}", string, padding)
7216 };
7217
7218 Ok(JValue::string(result))
7219 }
7220
7221 "trim" => {
7222 if evaluated_args.is_empty() {
7223 return Ok(JValue::Null); // undefined
7224 }
7225 if evaluated_args.len() != 1 {
7226 return Err(EvaluatorError::EvaluationError(
7227 "trim() requires at most 1 argument".to_string(),
7228 ));
7229 }
7230 match &evaluated_args[0] {
7231 JValue::Null => Ok(JValue::Null),
7232 JValue::String(s) => Ok(functions::string::trim(s)?),
7233 _ => Err(EvaluatorError::TypeError(
7234 "trim() requires a string argument".to_string(),
7235 )),
7236 }
7237 }
7238 "contains" => {
7239 if evaluated_args.len() != 2 {
7240 return Err(EvaluatorError::EvaluationError(
7241 "contains() requires exactly 2 arguments".to_string(),
7242 ));
7243 }
7244 if evaluated_args[0].is_null() {
7245 return Ok(JValue::Null);
7246 }
7247 if evaluated_args[0].is_undefined() {
7248 return Ok(JValue::Undefined);
7249 }
7250 match &evaluated_args[0] {
7251 JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
7252 _ => Err(EvaluatorError::TypeError(
7253 "contains() requires a string as the first argument".to_string(),
7254 )),
7255 }
7256 }
7257 "split" => {
7258 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7259 return Err(EvaluatorError::EvaluationError(
7260 "split() requires 2 or 3 arguments".to_string(),
7261 ));
7262 }
7263 if evaluated_args[0].is_null() {
7264 return Ok(JValue::Null);
7265 }
7266 if evaluated_args[0].is_undefined() {
7267 return Ok(JValue::Undefined);
7268 }
7269 match &evaluated_args[0] {
7270 JValue::String(s) => {
7271 let limit = if evaluated_args.len() == 3 {
7272 match &evaluated_args[2] {
7273 JValue::Number(n) => {
7274 let f = *n;
7275 // Negative limit is an error
7276 if f < 0.0 {
7277 return Err(EvaluatorError::EvaluationError(
7278 "D3020: Third argument of split function must be a positive number".to_string(),
7279 ));
7280 }
7281 // Floor the value for non-integer limits
7282 Some(f.floor() as usize)
7283 }
7284 _ => {
7285 return Err(EvaluatorError::TypeError(
7286 "split() limit must be a number".to_string(),
7287 ))
7288 }
7289 }
7290 } else {
7291 None
7292 };
7293 Ok(functions::string::split(s, &evaluated_args[1], limit)?)
7294 }
7295 _ => Err(EvaluatorError::TypeError(
7296 "split() requires a string as the first argument".to_string(),
7297 )),
7298 }
7299 }
7300 "join" => {
7301 // Special case: if first arg is undefined, return undefined
7302 // But if separator (2nd arg) is undefined, use empty string (default)
7303 if evaluated_args.is_empty() {
7304 return Err(EvaluatorError::TypeError(
7305 "T0410: Argument 1 of function $join does not match function signature"
7306 .to_string(),
7307 ));
7308 }
7309 if evaluated_args[0].is_null() {
7310 return Ok(JValue::Null);
7311 }
7312 if evaluated_args[0].is_undefined() {
7313 return Ok(JValue::Undefined);
7314 }
7315
7316 // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
7317 // The signature handles coercion and validation
7318 use crate::signature::Signature;
7319
7320 let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
7321 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7322 })?;
7323
7324 let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
7325 Ok(args) => args,
7326 Err(crate::signature::SignatureError::UndefinedArgument) => {
7327 // This can happen if the separator is undefined
7328 // In that case, just validate the first arg and use default separator
7329 let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
7330 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7331 })?;
7332
7333 match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
7334 Ok(args) => args,
7335 Err(crate::signature::SignatureError::ArrayTypeMismatch {
7336 index,
7337 expected,
7338 }) => {
7339 return Err(EvaluatorError::TypeError(format!(
7340 "T0412: Argument {} of function $join must be an array of {}",
7341 index, expected
7342 )));
7343 }
7344 Err(e) => {
7345 return Err(EvaluatorError::TypeError(format!(
7346 "Signature validation failed: {}",
7347 e
7348 )));
7349 }
7350 }
7351 }
7352 Err(crate::signature::SignatureError::ArgumentTypeMismatch {
7353 index,
7354 expected,
7355 }) => {
7356 return Err(EvaluatorError::TypeError(
7357 format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
7358 ));
7359 }
7360 Err(crate::signature::SignatureError::ArrayTypeMismatch {
7361 index,
7362 expected,
7363 }) => {
7364 return Err(EvaluatorError::TypeError(format!(
7365 "T0412: Argument {} of function $join must be an array of {}",
7366 index, expected
7367 )));
7368 }
7369 Err(e) => {
7370 return Err(EvaluatorError::TypeError(format!(
7371 "Signature validation failed: {}",
7372 e
7373 )));
7374 }
7375 };
7376
7377 // After coercion, first arg is guaranteed to be an array of strings
7378 match &coerced_args[0] {
7379 JValue::Array(arr) => {
7380 let separator = if coerced_args.len() == 2 {
7381 match &coerced_args[1] {
7382 JValue::String(s) => Some(&**s),
7383 JValue::Null => None, // Undefined separator -> use empty string
7384 _ => None, // Signature should have validated this
7385 }
7386 } else {
7387 None // No separator provided -> use empty string
7388 };
7389 Ok(functions::string::join(arr, separator)?)
7390 }
7391 JValue::Null => Ok(JValue::Null),
7392 _ => unreachable!("Signature validation should ensure array type"),
7393 }
7394 }
7395 "replace" => {
7396 if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
7397 return Err(EvaluatorError::EvaluationError(
7398 "replace() requires 3 or 4 arguments".to_string(),
7399 ));
7400 }
7401 if evaluated_args[0].is_null() {
7402 return Ok(JValue::Null);
7403 }
7404 if evaluated_args[0].is_undefined() {
7405 return Ok(JValue::Undefined);
7406 }
7407
7408 // Check if replacement (3rd arg) is a function/lambda
7409 let replacement_is_lambda = matches!(
7410 evaluated_args[2],
7411 JValue::Lambda { .. } | JValue::Builtin { .. }
7412 );
7413
7414 if replacement_is_lambda {
7415 // Lambda replacement mode
7416 return self.replace_with_lambda(
7417 &evaluated_args[0],
7418 &evaluated_args[1],
7419 &evaluated_args[2],
7420 if evaluated_args.len() == 4 {
7421 Some(&evaluated_args[3])
7422 } else {
7423 None
7424 },
7425 data,
7426 );
7427 }
7428
7429 // String replacement mode
7430 match (&evaluated_args[0], &evaluated_args[2]) {
7431 (JValue::String(s), JValue::String(replacement)) => {
7432 let limit = if evaluated_args.len() == 4 {
7433 match &evaluated_args[3] {
7434 JValue::Number(n) => {
7435 let lim_f64 = *n;
7436 if lim_f64 < 0.0 {
7437 return Err(EvaluatorError::EvaluationError(format!(
7438 "D3011: Limit must be non-negative, got {}",
7439 lim_f64
7440 )));
7441 }
7442 Some(lim_f64 as usize)
7443 }
7444 _ => {
7445 return Err(EvaluatorError::TypeError(
7446 "replace() limit must be a number".to_string(),
7447 ))
7448 }
7449 }
7450 } else {
7451 None
7452 };
7453 Ok(functions::string::replace(
7454 s,
7455 &evaluated_args[1],
7456 replacement,
7457 limit,
7458 )?)
7459 }
7460 _ => Err(EvaluatorError::TypeError(
7461 "replace() requires string arguments".to_string(),
7462 )),
7463 }
7464 }
7465 "match" => {
7466 // $match(str, pattern [, limit])
7467 // Returns array of match objects for regex matches or custom matcher function
7468 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7469 return Err(EvaluatorError::EvaluationError(
7470 "match() requires 1 to 3 arguments".to_string(),
7471 ));
7472 }
7473 if evaluated_args[0].is_null() {
7474 return Ok(JValue::Null);
7475 }
7476 if evaluated_args[0].is_undefined() {
7477 return Ok(JValue::Undefined);
7478 }
7479
7480 let s = match &evaluated_args[0] {
7481 JValue::String(s) => s.clone(),
7482 _ => {
7483 return Err(EvaluatorError::TypeError(
7484 "match() first argument must be a string".to_string(),
7485 ))
7486 }
7487 };
7488
7489 // Get optional limit
7490 let limit = if evaluated_args.len() == 3 {
7491 match &evaluated_args[2] {
7492 JValue::Number(n) => Some(*n as usize),
7493 JValue::Null => None,
7494 _ => {
7495 return Err(EvaluatorError::TypeError(
7496 "match() limit must be a number".to_string(),
7497 ))
7498 }
7499 }
7500 } else {
7501 None
7502 };
7503
7504 // Check if second argument is a custom matcher function (lambda)
7505 let pattern_value = evaluated_args.get(1);
7506 let is_custom_matcher = pattern_value.is_some_and(|val| {
7507 matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7508 });
7509
7510 if is_custom_matcher {
7511 // Custom matcher function support
7512 // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7513 return self.match_with_custom_matcher(&s, &args[1], limit, data);
7514 }
7515
7516 // Get regex pattern from second argument
7517 let (pattern, flags) = match pattern_value {
7518 Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7519 EvaluatorError::TypeError(
7520 "match() second argument must be a regex pattern or matcher function"
7521 .to_string(),
7522 )
7523 })?,
7524 None => (".*".to_string(), "".to_string()),
7525 };
7526
7527 // Build regex
7528 let is_global = flags.contains('g');
7529 let regex_pattern = if flags.contains('i') {
7530 format!("(?i){}", pattern)
7531 } else {
7532 pattern.clone()
7533 };
7534
7535 let re = regex::Regex::new(®ex_pattern).map_err(|e| {
7536 EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7537 })?;
7538
7539 let mut results = Vec::new();
7540 let mut count = 0;
7541
7542 for caps in re.captures_iter(&s) {
7543 if let Some(lim) = limit {
7544 if count >= lim {
7545 break;
7546 }
7547 }
7548
7549 let full_match = caps.get(0).unwrap();
7550 let mut match_obj = IndexMap::new();
7551 match_obj.insert(
7552 "match".to_string(),
7553 JValue::string(full_match.as_str().to_string()),
7554 );
7555 match_obj.insert(
7556 "index".to_string(),
7557 JValue::Number(full_match.start() as f64),
7558 );
7559
7560 // Collect capture groups
7561 let mut groups: Vec<JValue> = Vec::new();
7562 for i in 1..caps.len() {
7563 if let Some(group) = caps.get(i) {
7564 groups.push(JValue::string(group.as_str().to_string()));
7565 } else {
7566 groups.push(JValue::Null);
7567 }
7568 }
7569 if !groups.is_empty() {
7570 match_obj.insert("groups".to_string(), JValue::array(groups));
7571 }
7572
7573 results.push(JValue::object(match_obj));
7574 count += 1;
7575
7576 // If not global, only return first match
7577 if !is_global {
7578 break;
7579 }
7580 }
7581
7582 if results.is_empty() {
7583 Ok(JValue::Null)
7584 } else if results.len() == 1 && !is_global {
7585 // Single match (non-global) returns the match object directly
7586 Ok(results.into_iter().next().unwrap())
7587 } else {
7588 Ok(JValue::array(results))
7589 }
7590 }
7591 "max" => {
7592 if evaluated_args.len() != 1 {
7593 return Err(EvaluatorError::EvaluationError(
7594 "max() requires exactly 1 argument".to_string(),
7595 ));
7596 }
7597 // Check for undefined
7598 if evaluated_args[0].is_undefined() {
7599 return Ok(JValue::Undefined);
7600 }
7601 match &evaluated_args[0] {
7602 JValue::Null => Ok(JValue::Null),
7603 JValue::Array(arr) => {
7604 // Use zero-clone iterator-based max
7605 Ok(aggregation::max(arr)?)
7606 }
7607 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7608 _ => Err(EvaluatorError::TypeError(
7609 "max() requires an array or number argument".to_string(),
7610 )),
7611 }
7612 }
7613 "min" => {
7614 if evaluated_args.len() != 1 {
7615 return Err(EvaluatorError::EvaluationError(
7616 "min() requires exactly 1 argument".to_string(),
7617 ));
7618 }
7619 // Check for undefined
7620 if evaluated_args[0].is_undefined() {
7621 return Ok(JValue::Undefined);
7622 }
7623 match &evaluated_args[0] {
7624 JValue::Null => Ok(JValue::Null),
7625 JValue::Array(arr) => {
7626 // Use zero-clone iterator-based min
7627 Ok(aggregation::min(arr)?)
7628 }
7629 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7630 _ => Err(EvaluatorError::TypeError(
7631 "min() requires an array or number argument".to_string(),
7632 )),
7633 }
7634 }
7635 "average" => {
7636 if evaluated_args.len() != 1 {
7637 return Err(EvaluatorError::EvaluationError(
7638 "average() requires exactly 1 argument".to_string(),
7639 ));
7640 }
7641 // Return undefined if argument is undefined
7642 if evaluated_args[0].is_undefined() {
7643 return Ok(JValue::Undefined);
7644 }
7645 match &evaluated_args[0] {
7646 JValue::Null => Ok(JValue::Null),
7647 JValue::Array(arr) => {
7648 // Use zero-clone iterator-based average
7649 Ok(aggregation::average(arr)?)
7650 }
7651 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7652 _ => Err(EvaluatorError::TypeError(
7653 "average() requires an array or number argument".to_string(),
7654 )),
7655 }
7656 }
7657 "abs" => {
7658 if evaluated_args.len() != 1 {
7659 return Err(EvaluatorError::EvaluationError(
7660 "abs() requires exactly 1 argument".to_string(),
7661 ));
7662 }
7663 match &evaluated_args[0] {
7664 JValue::Null => Ok(JValue::Null),
7665 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
7666 _ => Err(EvaluatorError::TypeError(
7667 "abs() requires a number argument".to_string(),
7668 )),
7669 }
7670 }
7671 "floor" => {
7672 if evaluated_args.len() != 1 {
7673 return Err(EvaluatorError::EvaluationError(
7674 "floor() requires exactly 1 argument".to_string(),
7675 ));
7676 }
7677 match &evaluated_args[0] {
7678 JValue::Null => Ok(JValue::Null),
7679 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
7680 _ => Err(EvaluatorError::TypeError(
7681 "floor() requires a number argument".to_string(),
7682 )),
7683 }
7684 }
7685 "ceil" => {
7686 if evaluated_args.len() != 1 {
7687 return Err(EvaluatorError::EvaluationError(
7688 "ceil() requires exactly 1 argument".to_string(),
7689 ));
7690 }
7691 match &evaluated_args[0] {
7692 JValue::Null => Ok(JValue::Null),
7693 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
7694 _ => Err(EvaluatorError::TypeError(
7695 "ceil() requires a number argument".to_string(),
7696 )),
7697 }
7698 }
7699 "round" => {
7700 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7701 return Err(EvaluatorError::EvaluationError(
7702 "round() requires 1 or 2 arguments".to_string(),
7703 ));
7704 }
7705 match &evaluated_args[0] {
7706 JValue::Null => Ok(JValue::Null),
7707 JValue::Number(n) => {
7708 let precision = if evaluated_args.len() == 2 {
7709 match &evaluated_args[1] {
7710 JValue::Number(p) => Some(*p as i32),
7711 _ => {
7712 return Err(EvaluatorError::TypeError(
7713 "round() precision must be a number".to_string(),
7714 ))
7715 }
7716 }
7717 } else {
7718 None
7719 };
7720 Ok(functions::numeric::round(*n, precision)?)
7721 }
7722 _ => Err(EvaluatorError::TypeError(
7723 "round() requires a number argument".to_string(),
7724 )),
7725 }
7726 }
7727 "sqrt" => {
7728 if evaluated_args.len() != 1 {
7729 return Err(EvaluatorError::EvaluationError(
7730 "sqrt() requires exactly 1 argument".to_string(),
7731 ));
7732 }
7733 match &evaluated_args[0] {
7734 JValue::Null => Ok(JValue::Null),
7735 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
7736 _ => Err(EvaluatorError::TypeError(
7737 "sqrt() requires a number argument".to_string(),
7738 )),
7739 }
7740 }
7741 "power" => {
7742 if evaluated_args.len() != 2 {
7743 return Err(EvaluatorError::EvaluationError(
7744 "power() requires exactly 2 arguments".to_string(),
7745 ));
7746 }
7747 if evaluated_args[0].is_null() {
7748 return Ok(JValue::Null);
7749 }
7750 if evaluated_args[0].is_undefined() {
7751 return Ok(JValue::Undefined);
7752 }
7753 match (&evaluated_args[0], &evaluated_args[1]) {
7754 (JValue::Number(base), JValue::Number(exp)) => {
7755 Ok(functions::numeric::power(*base, *exp)?)
7756 }
7757 _ => Err(EvaluatorError::TypeError(
7758 "power() requires number arguments".to_string(),
7759 )),
7760 }
7761 }
7762 "formatNumber" => {
7763 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7764 return Err(EvaluatorError::EvaluationError(
7765 "formatNumber() requires 2 or 3 arguments".to_string(),
7766 ));
7767 }
7768 if evaluated_args[0].is_null() {
7769 return Ok(JValue::Null);
7770 }
7771 if evaluated_args[0].is_undefined() {
7772 return Ok(JValue::Undefined);
7773 }
7774 match (&evaluated_args[0], &evaluated_args[1]) {
7775 (JValue::Number(num), JValue::String(picture)) => {
7776 let options = if evaluated_args.len() == 3 {
7777 Some(&evaluated_args[2])
7778 } else {
7779 None
7780 };
7781 Ok(functions::numeric::format_number(*num, picture, options)?)
7782 }
7783 _ => Err(EvaluatorError::TypeError(
7784 "formatNumber() requires a number and a string".to_string(),
7785 )),
7786 }
7787 }
7788 "formatBase" => {
7789 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7790 return Err(EvaluatorError::EvaluationError(
7791 "formatBase() requires 1 or 2 arguments".to_string(),
7792 ));
7793 }
7794 // Handle undefined input
7795 if evaluated_args[0].is_null() {
7796 return Ok(JValue::Null);
7797 }
7798 if evaluated_args[0].is_undefined() {
7799 return Ok(JValue::Undefined);
7800 }
7801 match &evaluated_args[0] {
7802 JValue::Number(num) => {
7803 let radix = if evaluated_args.len() == 2 {
7804 match &evaluated_args[1] {
7805 JValue::Number(r) => Some(r.trunc() as i64),
7806 _ => {
7807 return Err(EvaluatorError::TypeError(
7808 "formatBase() radix must be a number".to_string(),
7809 ))
7810 }
7811 }
7812 } else {
7813 None
7814 };
7815 Ok(functions::numeric::format_base(*num, radix)?)
7816 }
7817 _ => Err(EvaluatorError::TypeError(
7818 "formatBase() requires a number".to_string(),
7819 )),
7820 }
7821 }
7822 "formatInteger" => {
7823 if evaluated_args.len() != 2 {
7824 return Err(EvaluatorError::EvaluationError(
7825 "formatInteger() requires exactly 2 arguments".to_string(),
7826 ));
7827 }
7828 match (&evaluated_args[0], &evaluated_args[1]) {
7829 (JValue::Number(n), JValue::String(picture)) => {
7830 Ok(crate::datetime::format_integer(*n, picture)?)
7831 }
7832 (JValue::Null, _) => Ok(JValue::Null),
7833 (JValue::Undefined, _) => Ok(JValue::Undefined),
7834 _ => Err(EvaluatorError::TypeError(
7835 "formatInteger() requires a number and a string".to_string(),
7836 )),
7837 }
7838 }
7839 "parseInteger" => {
7840 if evaluated_args.len() != 2 {
7841 return Err(EvaluatorError::EvaluationError(
7842 "parseInteger() requires exactly 2 arguments".to_string(),
7843 ));
7844 }
7845 match (&evaluated_args[0], &evaluated_args[1]) {
7846 (JValue::String(value), JValue::String(picture)) => {
7847 Ok(crate::datetime::parse_integer(value, picture)?)
7848 }
7849 (JValue::Null, _) => Ok(JValue::Null),
7850 (JValue::Undefined, _) => Ok(JValue::Undefined),
7851 _ => Err(EvaluatorError::TypeError(
7852 "parseInteger() requires a string and a string".to_string(),
7853 )),
7854 }
7855 }
7856 "append" => {
7857 if evaluated_args.len() != 2 {
7858 return Err(EvaluatorError::EvaluationError(
7859 "append() requires exactly 2 arguments".to_string(),
7860 ));
7861 }
7862 // Handle null/undefined arguments
7863 let first = &evaluated_args[0];
7864 let second = &evaluated_args[1];
7865
7866 // If second arg is null/undefined, return first as-is (no change)
7867 if second.is_null() || second.is_undefined() {
7868 return Ok(first.clone());
7869 }
7870
7871 // If first arg is null/undefined, return second as-is (appending to nothing gives second)
7872 if first.is_null() || first.is_undefined() {
7873 return Ok(second.clone());
7874 }
7875
7876 // Convert both to arrays if needed, then append
7877 let arr = match first {
7878 JValue::Array(a) => a.to_vec(),
7879 other => vec![other.clone()], // Wrap non-array in array
7880 };
7881
7882 // Pre-check combined size before concatenating, mirroring
7883 // jsonata-js's append() (`arg1.length + arg2.length > options.sequence`).
7884 let second_len = match second {
7885 JValue::Array(a) => a.len(),
7886 _ => 1,
7887 };
7888 check_sequence_length(arr.len() + second_len, &self.options)?;
7889
7890 Ok(functions::array::append(&arr, second)?)
7891 }
7892 "reverse" => {
7893 if evaluated_args.len() != 1 {
7894 return Err(EvaluatorError::EvaluationError(
7895 "reverse() requires exactly 1 argument".to_string(),
7896 ));
7897 }
7898 match &evaluated_args[0] {
7899 JValue::Null => Ok(JValue::Null),
7900 JValue::Undefined => Ok(JValue::Undefined),
7901 JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
7902 _ => Err(EvaluatorError::TypeError(
7903 "reverse() requires an array argument".to_string(),
7904 )),
7905 }
7906 }
7907 "shuffle" => {
7908 if evaluated_args.len() != 1 {
7909 return Err(EvaluatorError::EvaluationError(
7910 "shuffle() requires exactly 1 argument".to_string(),
7911 ));
7912 }
7913 if evaluated_args[0].is_null() {
7914 return Ok(JValue::Null);
7915 }
7916 if evaluated_args[0].is_undefined() {
7917 return Ok(JValue::Undefined);
7918 }
7919 match &evaluated_args[0] {
7920 JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
7921 _ => Err(EvaluatorError::TypeError(
7922 "shuffle() requires an array argument".to_string(),
7923 )),
7924 }
7925 }
7926
7927 "sift" => {
7928 // $sift(object, function) or $sift(function) - filter object by predicate
7929 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7930 return Err(EvaluatorError::EvaluationError(
7931 "sift() requires 1 or 2 arguments".to_string(),
7932 ));
7933 }
7934
7935 // Determine which argument is the function
7936 let func_arg = if evaluated_args.len() == 1 {
7937 &args[0]
7938 } else {
7939 &args[1]
7940 };
7941
7942 // Detect how many parameters the callback expects
7943 let param_count = self.get_callback_param_count(func_arg);
7944
7945 // Helper function to sift a single object
7946 let sift_object = |evaluator: &mut Self,
7947 obj: &IndexMap<String, JValue>,
7948 func_node: &AstNode,
7949 context_data: &JValue,
7950 param_count: usize|
7951 -> Result<JValue, EvaluatorError> {
7952 // Only create the object value if callback uses 3 parameters
7953 let obj_value = if param_count >= 3 {
7954 Some(JValue::object(obj.clone()))
7955 } else {
7956 None
7957 };
7958
7959 let mut result = IndexMap::new();
7960 for (key, value) in obj.iter() {
7961 // Build argument list based on what callback expects
7962 let call_args = match param_count {
7963 1 => vec![value.clone()],
7964 2 => vec![value.clone(), JValue::string(key.clone())],
7965 _ => vec![
7966 value.clone(),
7967 JValue::string(key.clone()),
7968 obj_value.as_ref().unwrap().clone(),
7969 ],
7970 };
7971
7972 let pred_result =
7973 evaluator.apply_function(func_node, &call_args, context_data)?;
7974 if evaluator.is_truthy(&pred_result) {
7975 result.insert(key.clone(), value.clone());
7976 }
7977 }
7978 // Return undefined for empty results (will be filtered by function application)
7979 if result.is_empty() {
7980 Ok(JValue::Undefined)
7981 } else {
7982 Ok(JValue::object(result))
7983 }
7984 };
7985
7986 // Handle partial application - if only 1 arg, use current context as object
7987 if evaluated_args.len() == 1 {
7988 // $sift(function) - use current context data as object
7989 match data {
7990 JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
7991 JValue::Array(arr) => {
7992 // Map sift over each object in the array
7993 let mut results = Vec::new();
7994 for item in arr.iter() {
7995 if let JValue::Object(o) = item {
7996 let sifted = sift_object(self, o, &args[0], item, param_count)?;
7997 // sift_object returns undefined for empty results
7998 if !sifted.is_undefined() {
7999 results.push(sifted);
8000 }
8001 }
8002 }
8003 Ok(JValue::array(results))
8004 }
8005 JValue::Null => Ok(JValue::Null),
8006 _ => Ok(JValue::Undefined),
8007 }
8008 } else {
8009 // $sift(object, function)
8010 match &evaluated_args[0] {
8011 JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
8012 JValue::Null => Ok(JValue::Null),
8013 _ => Err(EvaluatorError::TypeError(
8014 "sift() first argument must be an object".to_string(),
8015 )),
8016 }
8017 }
8018 }
8019
8020 "zip" => {
8021 if evaluated_args.is_empty() {
8022 return Err(EvaluatorError::EvaluationError(
8023 "zip() requires at least 1 argument".to_string(),
8024 ));
8025 }
8026
8027 // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
8028 // If any argument is null/undefined, return empty array
8029 let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
8030 for arg in &evaluated_args {
8031 match arg {
8032 JValue::Array(arr) => {
8033 if arr.is_empty() {
8034 // Empty array means result is empty
8035 return Ok(JValue::array(vec![]));
8036 }
8037 arrays.push(arr.to_vec());
8038 }
8039 JValue::Null | JValue::Undefined => {
8040 // Null/undefined means result is empty
8041 return Ok(JValue::array(vec![]));
8042 }
8043 other => {
8044 // Wrap non-array values in single-element array
8045 arrays.push(vec![other.clone()]);
8046 }
8047 }
8048 }
8049
8050 if arrays.is_empty() {
8051 return Ok(JValue::array(vec![]));
8052 }
8053
8054 // Find the length of the shortest array
8055 let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
8056
8057 // Zip the arrays together
8058 let mut result = Vec::with_capacity(min_len);
8059 for i in 0..min_len {
8060 let mut tuple = Vec::with_capacity(arrays.len());
8061 for array in &arrays {
8062 tuple.push(array[i].clone());
8063 }
8064 result.push(JValue::array(tuple));
8065 }
8066
8067 Ok(JValue::array(result))
8068 }
8069
8070 "sort" => {
8071 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8072 return Err(EvaluatorError::EvaluationError(
8073 "sort() requires 1 or 2 arguments".to_string(),
8074 ));
8075 }
8076
8077 // Use pre-evaluated first argument (avoid double evaluation)
8078 let array_value = &evaluated_args[0];
8079
8080 // Handle undefined input
8081 if array_value.is_null() {
8082 return Ok(JValue::Null);
8083 }
8084 if array_value.is_undefined() {
8085 return Ok(JValue::Undefined);
8086 }
8087
8088 let mut arr = match array_value {
8089 JValue::Array(arr) => arr.to_vec(),
8090 other => vec![other.clone()],
8091 };
8092
8093 if args.len() == 2 {
8094 // Sort using the comparator from raw args (need unevaluated lambda AST)
8095 // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
8096 self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
8097 Ok(JValue::array(arr))
8098 } else {
8099 // Default sort (no comparator)
8100 Ok(functions::array::sort(&arr)?)
8101 }
8102 }
8103 "distinct" => {
8104 if evaluated_args.len() != 1 {
8105 return Err(EvaluatorError::EvaluationError(
8106 "distinct() requires exactly 1 argument".to_string(),
8107 ));
8108 }
8109 match &evaluated_args[0] {
8110 JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
8111 // Non-array input, and arrays of length <= 1, pass through
8112 // unchanged (jsonata-js functions.js:
8113 // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
8114 other => Ok(other.clone()),
8115 }
8116 }
8117 "exists" => {
8118 if evaluated_args.len() != 1 {
8119 return Err(EvaluatorError::EvaluationError(
8120 "exists() requires exactly 1 argument".to_string(),
8121 ));
8122 }
8123 Ok(functions::array::exists(&evaluated_args[0])?)
8124 }
8125 "keys" => {
8126 if evaluated_args.len() != 1 {
8127 return Err(EvaluatorError::EvaluationError(
8128 "keys() requires exactly 1 argument".to_string(),
8129 ));
8130 }
8131
8132 // Helper to unwrap single-element arrays
8133 let unwrap_single = |keys: Vec<JValue>| -> JValue {
8134 if keys.len() == 1 {
8135 keys.into_iter().next().unwrap()
8136 } else {
8137 JValue::array(keys)
8138 }
8139 };
8140
8141 match &evaluated_args[0] {
8142 JValue::Null => Ok(JValue::Null),
8143 JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
8144 JValue::Object(obj) => {
8145 // Return undefined for empty objects
8146 if obj.is_empty() {
8147 Ok(JValue::Null)
8148 } else {
8149 let keys: Vec<JValue> =
8150 obj.keys().map(|k| JValue::string(k.clone())).collect();
8151 check_sequence_length(keys.len(), &self.options)?;
8152 Ok(unwrap_single(keys))
8153 }
8154 }
8155 JValue::Array(arr) => {
8156 // For arrays, collect keys from all objects
8157 let mut all_keys = Vec::new();
8158 for item in arr.iter() {
8159 // Skip lambda/builtin values
8160 if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
8161 continue;
8162 }
8163 if let JValue::Object(obj) = item {
8164 for key in obj.keys() {
8165 if !all_keys.contains(&JValue::string(key.clone())) {
8166 all_keys.push(JValue::string(key.clone()));
8167 }
8168 }
8169 }
8170 }
8171 if all_keys.is_empty() {
8172 Ok(JValue::Null)
8173 } else {
8174 check_sequence_length(all_keys.len(), &self.options)?;
8175 Ok(unwrap_single(all_keys))
8176 }
8177 }
8178 // Non-object types return undefined
8179 _ => Ok(JValue::Null),
8180 }
8181 }
8182 "lookup" => {
8183 if evaluated_args.len() != 2 {
8184 return Err(EvaluatorError::EvaluationError(
8185 "lookup() requires exactly 2 arguments".to_string(),
8186 ));
8187 }
8188 if evaluated_args[0].is_null() {
8189 return Ok(JValue::Null);
8190 }
8191 if evaluated_args[0].is_undefined() {
8192 return Ok(JValue::Undefined);
8193 }
8194
8195 let key = match &evaluated_args[1] {
8196 JValue::String(k) => &**k,
8197 _ => {
8198 return Err(EvaluatorError::TypeError(
8199 "lookup() requires a string key".to_string(),
8200 ))
8201 }
8202 };
8203
8204 // Helper function to recursively lookup in values
8205 fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
8206 match val {
8207 JValue::Array(arr) => {
8208 let mut results = Vec::new();
8209 for item in arr.iter() {
8210 let nested = lookup_recursive(item, key);
8211 results.extend(nested.iter().cloned());
8212 }
8213 results
8214 }
8215 JValue::Object(obj) => {
8216 if let Some(v) = obj.get(key) {
8217 vec![v.clone()]
8218 } else {
8219 vec![]
8220 }
8221 }
8222 _ => vec![],
8223 }
8224 }
8225
8226 let results = lookup_recursive(&evaluated_args[0], key);
8227 if results.is_empty() {
8228 Ok(JValue::Null)
8229 } else if results.len() == 1 {
8230 Ok(results[0].clone())
8231 } else {
8232 check_sequence_length(results.len(), &self.options)?;
8233 Ok(JValue::array(results))
8234 }
8235 }
8236 "spread" => {
8237 if evaluated_args.len() != 1 {
8238 return Err(EvaluatorError::EvaluationError(
8239 "spread() requires exactly 1 argument".to_string(),
8240 ));
8241 }
8242 match &evaluated_args[0] {
8243 JValue::Null => Ok(JValue::Null),
8244 // Not a container - pass through unchanged (e.g. so $string() still
8245 // sees the function value and applies its own function->"" rule).
8246 lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
8247 JValue::Object(obj) => {
8248 // functions::object::spread() always returns an array with one
8249 // element per key (mirrors jsonata-js's push-per-key loop through
8250 // this.createSequence()), so it needs the same cap as the
8251 // array-fanout branch below and as the "keys" arm's single-object
8252 // branch.
8253 check_sequence_length(obj.len(), &self.options)?;
8254 Ok(functions::object::spread(obj)?)
8255 }
8256 JValue::Array(arr) => {
8257 // Spread each object in the array
8258 let mut result = Vec::new();
8259 for item in arr.iter() {
8260 match item {
8261 JValue::Lambda { .. } | JValue::Builtin { .. } => {
8262 // Skip lambdas in array
8263 continue;
8264 }
8265 JValue::Object(obj) => {
8266 let spread_result = functions::object::spread(obj)?;
8267 if let JValue::Array(spread_items) = spread_result {
8268 result.extend(spread_items.iter().cloned());
8269 } else {
8270 result.push(spread_result);
8271 }
8272 }
8273 // Non-objects in array are returned unchanged
8274 other => result.push(other.clone()),
8275 }
8276 }
8277 check_sequence_length(result.len(), &self.options)?;
8278 Ok(JValue::array(result))
8279 }
8280 // Non-objects are returned unchanged
8281 other => Ok(other.clone()),
8282 }
8283 }
8284 "merge" => {
8285 if evaluated_args.is_empty() {
8286 return Err(EvaluatorError::EvaluationError(
8287 "merge() requires at least 1 argument".to_string(),
8288 ));
8289 }
8290 // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
8291 // vs multiple object arguments: $merge(obj1, obj2)
8292 if evaluated_args.len() == 1 {
8293 match &evaluated_args[0] {
8294 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
8295 JValue::Null => Ok(JValue::Null),
8296 JValue::Undefined => Ok(JValue::Undefined),
8297 JValue::Object(_) => {
8298 // Single object - just return it
8299 Ok(evaluated_args[0].clone())
8300 }
8301 _ => Err(EvaluatorError::TypeError(
8302 "merge() requires objects or an array of objects".to_string(),
8303 )),
8304 }
8305 } else {
8306 Ok(functions::object::merge(&evaluated_args)?)
8307 }
8308 }
8309
8310 "map" => {
8311 if args.len() != 2 {
8312 return Err(EvaluatorError::EvaluationError(
8313 "map() requires exactly 2 arguments".to_string(),
8314 ));
8315 }
8316
8317 // Evaluate the array argument
8318 let array = self.evaluate_internal(&args[0], data)?;
8319
8320 match array {
8321 JValue::Array(arr) => {
8322 // Detect how many parameters the callback expects
8323 let param_count = self.get_callback_param_count(&args[1]);
8324
8325 // CompiledExpr fast path: direct lambda with 1 param, compilable body
8326 if param_count == 1 {
8327 if let AstNode::Lambda {
8328 params,
8329 body,
8330 signature: None,
8331 thunk: false,
8332 } = &args[1]
8333 {
8334 let var_refs: Vec<&str> =
8335 params.iter().map(|s| s.as_str()).collect();
8336 if let Some(compiled) =
8337 try_compile_expr_with_allowed_vars(body, &var_refs)
8338 {
8339 let param_name = params[0].as_str();
8340 let mut result = Vec::with_capacity(arr.len());
8341 let mut vars = HashMap::new();
8342 for item in arr.iter() {
8343 vars.insert(param_name, item);
8344 let mapped = eval_compiled(
8345 &compiled,
8346 data,
8347 Some(&vars),
8348 &self.options,
8349 self.start_time,
8350 )?;
8351 if !mapped.is_undefined() {
8352 result.push(mapped);
8353 }
8354 }
8355 check_sequence_length(result.len(), &self.options)?;
8356 return Ok(JValue::array(result));
8357 }
8358 }
8359 // Stored lambda variable fast path: $var with pre-compiled body
8360 if let AstNode::Variable(var_name) = &args[1] {
8361 if let Some(stored) = self.context.lookup_lambda(var_name) {
8362 if let Some(ref ce) = stored.compiled_body.clone() {
8363 let param_name = stored.params[0].clone();
8364 let captured_data = stored.captured_data.clone();
8365 let captured_env_clone = stored.captured_env.clone();
8366 let ce_clone = ce.clone();
8367 if !captured_env_clone.values().any(|v| {
8368 matches!(
8369 v,
8370 JValue::Lambda { .. } | JValue::Builtin { .. }
8371 )
8372 }) {
8373 let call_data = captured_data.as_ref().unwrap_or(data);
8374 let mut result = Vec::with_capacity(arr.len());
8375 let mut vars: HashMap<&str, &JValue> =
8376 captured_env_clone
8377 .iter()
8378 .map(|(k, v)| (k.as_str(), v))
8379 .collect();
8380 for item in arr.iter() {
8381 vars.insert(param_name.as_str(), item);
8382 let mapped = eval_compiled(
8383 &ce_clone,
8384 call_data,
8385 Some(&vars),
8386 &self.options,
8387 self.start_time,
8388 )?;
8389 if !mapped.is_undefined() {
8390 result.push(mapped);
8391 }
8392 }
8393 check_sequence_length(result.len(), &self.options)?;
8394 return Ok(JValue::array(result));
8395 }
8396 }
8397 }
8398 }
8399 }
8400
8401 // Only create the array value if callback uses 3 parameters
8402 let arr_value = if param_count >= 3 {
8403 Some(JValue::Array(arr.clone()))
8404 } else {
8405 None
8406 };
8407
8408 let mut result = Vec::with_capacity(arr.len());
8409 for (index, item) in arr.iter().enumerate() {
8410 // Build argument list based on what callback expects
8411 let call_args = match param_count {
8412 1 => vec![item.clone()],
8413 2 => vec![item.clone(), JValue::Number(index as f64)],
8414 _ => vec![
8415 item.clone(),
8416 JValue::Number(index as f64),
8417 arr_value.as_ref().unwrap().clone(),
8418 ],
8419 };
8420
8421 let mapped = self.apply_function(&args[1], &call_args, data)?;
8422 // Filter out undefined results but keep explicit null (JSONata map semantics)
8423 // undefined comes from missing else clause, null is explicit
8424 if !mapped.is_undefined() {
8425 result.push(mapped);
8426 }
8427 }
8428 check_sequence_length(result.len(), &self.options)?;
8429 Ok(JValue::array(result))
8430 }
8431 JValue::Null => Ok(JValue::Null),
8432 JValue::Undefined => Ok(JValue::Undefined),
8433 _ => Err(EvaluatorError::TypeError(
8434 "map() first argument must be an array".to_string(),
8435 )),
8436 }
8437 }
8438
8439 "filter" => {
8440 if args.len() != 2 {
8441 return Err(EvaluatorError::EvaluationError(
8442 "filter() requires exactly 2 arguments".to_string(),
8443 ));
8444 }
8445
8446 // Evaluate the array argument
8447 let array = self.evaluate_internal(&args[0], data)?;
8448
8449 // Handle undefined input - return undefined
8450 if array.is_undefined() {
8451 return Ok(JValue::Undefined);
8452 }
8453
8454 // Handle null input
8455 if array.is_null() {
8456 return Ok(JValue::Undefined);
8457 }
8458
8459 // Coerce non-array values to single-element arrays
8460 // Track if input was a single value to unwrap result appropriately
8461 // Use references to avoid upfront cloning of all elements
8462 let single_holder;
8463 let (items, was_single_value): (&[JValue], bool) = match &array {
8464 JValue::Array(arr) => (arr.as_slice(), false),
8465 _ => {
8466 single_holder = [array];
8467 (&single_holder[..], true)
8468 }
8469 };
8470
8471 // Detect how many parameters the callback expects
8472 let param_count = self.get_callback_param_count(&args[1]);
8473
8474 // CompiledExpr fast path: direct lambda with 1 param, compilable body
8475 if param_count == 1 {
8476 if let AstNode::Lambda {
8477 params,
8478 body,
8479 signature: None,
8480 thunk: false,
8481 } = &args[1]
8482 {
8483 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8484 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8485 {
8486 let param_name = params[0].as_str();
8487 let mut result = Vec::with_capacity(items.len() / 2);
8488 let mut vars = HashMap::new();
8489 for item in items.iter() {
8490 vars.insert(param_name, item);
8491 let pred_result = eval_compiled(
8492 &compiled,
8493 data,
8494 Some(&vars),
8495 &self.options,
8496 self.start_time,
8497 )?;
8498 if compiled_is_truthy(&pred_result) {
8499 result.push(item.clone());
8500 }
8501 }
8502 if was_single_value {
8503 if result.len() == 1 {
8504 return Ok(result.remove(0));
8505 } else if result.is_empty() {
8506 return Ok(JValue::Undefined);
8507 }
8508 }
8509 check_sequence_length(result.len(), &self.options)?;
8510 return Ok(JValue::array(result));
8511 }
8512 }
8513 // Stored lambda variable fast path: $var with pre-compiled body
8514 if let AstNode::Variable(var_name) = &args[1] {
8515 if let Some(stored) = self.context.lookup_lambda(var_name) {
8516 if let Some(ref ce) = stored.compiled_body.clone() {
8517 let param_name = stored.params[0].clone();
8518 let captured_data = stored.captured_data.clone();
8519 let captured_env_clone = stored.captured_env.clone();
8520 let ce_clone = ce.clone();
8521 if !captured_env_clone.values().any(|v| {
8522 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8523 }) {
8524 let call_data = captured_data.as_ref().unwrap_or(data);
8525 let mut result = Vec::with_capacity(items.len() / 2);
8526 let mut vars: HashMap<&str, &JValue> = captured_env_clone
8527 .iter()
8528 .map(|(k, v)| (k.as_str(), v))
8529 .collect();
8530 for item in items.iter() {
8531 vars.insert(param_name.as_str(), item);
8532 let pred_result = eval_compiled(
8533 &ce_clone,
8534 call_data,
8535 Some(&vars),
8536 &self.options,
8537 self.start_time,
8538 )?;
8539 if compiled_is_truthy(&pred_result) {
8540 result.push(item.clone());
8541 }
8542 }
8543 if was_single_value {
8544 if result.len() == 1 {
8545 return Ok(result.remove(0));
8546 } else if result.is_empty() {
8547 return Ok(JValue::Undefined);
8548 }
8549 }
8550 check_sequence_length(result.len(), &self.options)?;
8551 return Ok(JValue::array(result));
8552 }
8553 }
8554 }
8555 }
8556 }
8557
8558 // Only create the array value if callback uses 3 parameters
8559 let arr_value = if param_count >= 3 {
8560 Some(JValue::array(items.to_vec()))
8561 } else {
8562 None
8563 };
8564
8565 let mut result = Vec::with_capacity(items.len() / 2);
8566
8567 for (index, item) in items.iter().enumerate() {
8568 // Build argument list based on what callback expects
8569 let call_args = match param_count {
8570 1 => vec![item.clone()],
8571 2 => vec![item.clone(), JValue::Number(index as f64)],
8572 _ => vec![
8573 item.clone(),
8574 JValue::Number(index as f64),
8575 arr_value.as_ref().unwrap().clone(),
8576 ],
8577 };
8578
8579 let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8580 if self.is_truthy(&predicate_result) {
8581 result.push(item.clone());
8582 }
8583 }
8584
8585 // If input was a single value, return the single matching item
8586 // (or undefined if no match)
8587 if was_single_value {
8588 if result.len() == 1 {
8589 return Ok(result.remove(0));
8590 } else if result.is_empty() {
8591 return Ok(JValue::Undefined);
8592 }
8593 }
8594
8595 check_sequence_length(result.len(), &self.options)?;
8596 Ok(JValue::array(result))
8597 }
8598
8599 "reduce" => {
8600 if args.len() < 2 || args.len() > 3 {
8601 return Err(EvaluatorError::EvaluationError(
8602 "reduce() requires 2 or 3 arguments".to_string(),
8603 ));
8604 }
8605
8606 // Check that the callback function has at least 2 parameters
8607 if let AstNode::Lambda { params, .. } = &args[1] {
8608 if params.len() < 2 {
8609 return Err(EvaluatorError::EvaluationError(
8610 "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
8611 ));
8612 }
8613 } else if let AstNode::Function { name, .. } = &args[1] {
8614 // For now, we can't validate built-in function signatures here
8615 // But user-defined functions via lambda will be validated above
8616 let _ = name; // avoid unused warning
8617 }
8618
8619 // Evaluate the array argument
8620 let array = self.evaluate_internal(&args[0], data)?;
8621
8622 // Convert single value to array (JSONata reduce accepts single values)
8623 // Use references to avoid upfront cloning of all elements
8624 let single_holder;
8625 let items: &[JValue] = match &array {
8626 JValue::Array(arr) => arr.as_slice(),
8627 JValue::Null => return Ok(JValue::Null),
8628 _ => {
8629 single_holder = [array];
8630 &single_holder[..]
8631 }
8632 };
8633
8634 if items.is_empty() {
8635 // Return initial value if provided, otherwise null
8636 return if args.len() == 3 {
8637 self.evaluate_internal(&args[2], data)
8638 } else {
8639 Ok(JValue::Null)
8640 };
8641 }
8642
8643 // Get initial accumulator
8644 let mut accumulator = if args.len() == 3 {
8645 self.evaluate_internal(&args[2], data)?
8646 } else {
8647 items[0].clone()
8648 };
8649
8650 let start_idx = if args.len() == 3 { 0 } else { 1 };
8651
8652 // Detect how many parameters the callback expects
8653 let param_count = self.get_callback_param_count(&args[1]);
8654
8655 // CompiledExpr fast path: direct lambda with 2 params, compilable body
8656 if param_count == 2 {
8657 if let AstNode::Lambda {
8658 params,
8659 body,
8660 signature: None,
8661 thunk: false,
8662 } = &args[1]
8663 {
8664 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8665 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8666 {
8667 let acc_name = params[0].as_str();
8668 let item_name = params[1].as_str();
8669 for item in items[start_idx..].iter() {
8670 let vars: HashMap<&str, &JValue> =
8671 HashMap::from([(acc_name, &accumulator), (item_name, item)]);
8672 accumulator = eval_compiled(
8673 &compiled,
8674 data,
8675 Some(&vars),
8676 &self.options,
8677 self.start_time,
8678 )?;
8679 }
8680 return Ok(accumulator);
8681 }
8682 }
8683 // Stored lambda variable fast path: $var with pre-compiled body
8684 if let AstNode::Variable(var_name) = &args[1] {
8685 if let Some(stored) = self.context.lookup_lambda(var_name) {
8686 if stored.params.len() == 2 {
8687 if let Some(ref ce) = stored.compiled_body.clone() {
8688 let acc_param = stored.params[0].clone();
8689 let item_param = stored.params[1].clone();
8690 let captured_data = stored.captured_data.clone();
8691 let captured_env_clone = stored.captured_env.clone();
8692 let ce_clone = ce.clone();
8693 if !captured_env_clone.values().any(|v| {
8694 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8695 }) {
8696 let call_data = captured_data.as_ref().unwrap_or(data);
8697 for item in items[start_idx..].iter() {
8698 let mut vars: HashMap<&str, &JValue> =
8699 captured_env_clone
8700 .iter()
8701 .map(|(k, v)| (k.as_str(), v))
8702 .collect();
8703 vars.insert(acc_param.as_str(), &accumulator);
8704 vars.insert(item_param.as_str(), item);
8705 // Evaluate and drop vars before assigning accumulator
8706 // to satisfy borrow checker (vars borrows accumulator)
8707 let new_acc = eval_compiled(
8708 &ce_clone,
8709 call_data,
8710 Some(&vars),
8711 &self.options,
8712 self.start_time,
8713 )?;
8714 drop(vars);
8715 accumulator = new_acc;
8716 }
8717 return Ok(accumulator);
8718 }
8719 }
8720 }
8721 }
8722 }
8723 }
8724
8725 // Only create the array value if callback uses 4 parameters
8726 let arr_value = if param_count >= 4 {
8727 Some(JValue::array(items.to_vec()))
8728 } else {
8729 None
8730 };
8731
8732 // Apply function to each element
8733 for (idx, item) in items[start_idx..].iter().enumerate() {
8734 // For reduce, the function receives (accumulator, value, index, array)
8735 // Callbacks may use any subset of these parameters
8736 let actual_idx = start_idx + idx;
8737
8738 // Build argument list based on what callback expects
8739 let call_args = match param_count {
8740 2 => vec![accumulator.clone(), item.clone()],
8741 3 => vec![
8742 accumulator.clone(),
8743 item.clone(),
8744 JValue::Number(actual_idx as f64),
8745 ],
8746 _ => vec![
8747 accumulator.clone(),
8748 item.clone(),
8749 JValue::Number(actual_idx as f64),
8750 arr_value.as_ref().unwrap().clone(),
8751 ],
8752 };
8753
8754 accumulator = self.apply_function(&args[1], &call_args, data)?;
8755 }
8756
8757 Ok(accumulator)
8758 }
8759
8760 "single" => {
8761 if args.is_empty() || args.len() > 2 {
8762 return Err(EvaluatorError::EvaluationError(
8763 "single() requires 1 or 2 arguments".to_string(),
8764 ));
8765 }
8766
8767 // Evaluate the array argument
8768 let array = self.evaluate_internal(&args[0], data)?;
8769
8770 // Convert to array (wrap single values)
8771 let arr = match array {
8772 JValue::Array(arr) => arr.to_vec(),
8773 JValue::Null => return Ok(JValue::Null),
8774 other => vec![other],
8775 };
8776
8777 if args.len() == 1 {
8778 // No predicate - array must have exactly 1 element
8779 match arr.len() {
8780 0 => Err(EvaluatorError::EvaluationError(
8781 "single() argument is empty".to_string(),
8782 )),
8783 1 => Ok(arr.into_iter().next().unwrap()),
8784 count => Err(EvaluatorError::EvaluationError(format!(
8785 "single() argument has {} values (expected exactly 1)",
8786 count
8787 ))),
8788 }
8789 } else {
8790 // With predicate - find exactly 1 matching element
8791 let arr_value = JValue::array(arr.clone());
8792 let mut matches = Vec::new();
8793 for (index, item) in arr.into_iter().enumerate() {
8794 // Apply predicate function with (item, index, array)
8795 let predicate_result = self.apply_function(
8796 &args[1],
8797 &[
8798 item.clone(),
8799 JValue::Number(index as f64),
8800 arr_value.clone(),
8801 ],
8802 data,
8803 )?;
8804 if self.is_truthy(&predicate_result) {
8805 matches.push(item);
8806 }
8807 }
8808
8809 match matches.len() {
8810 0 => Err(EvaluatorError::EvaluationError(
8811 "single() predicate matches no values".to_string(),
8812 )),
8813 1 => Ok(matches.into_iter().next().unwrap()),
8814 count => Err(EvaluatorError::EvaluationError(format!(
8815 "single() predicate matches {} values (expected exactly 1)",
8816 count
8817 ))),
8818 }
8819 }
8820 }
8821
8822 "each" => {
8823 // $each(object, function) - iterate over object, applying function to each value/key pair
8824 // Returns an array of the function results
8825 if args.is_empty() || args.len() > 2 {
8826 return Err(EvaluatorError::EvaluationError(
8827 "each() requires 1 or 2 arguments".to_string(),
8828 ));
8829 }
8830
8831 // Determine which argument is the object and which is the function
8832 let (obj_value, func_arg) = if args.len() == 1 {
8833 // Single argument: use current data as object
8834 (data.clone(), &args[0])
8835 } else {
8836 // Two arguments: first is object, second is function
8837 (self.evaluate_internal(&args[0], data)?, &args[1])
8838 };
8839
8840 // Detect how many parameters the callback expects
8841 let param_count = self.get_callback_param_count(func_arg);
8842
8843 match obj_value {
8844 JValue::Object(obj) => {
8845 let mut result = Vec::new();
8846 for (key, value) in obj.iter() {
8847 // Build argument list based on what callback expects
8848 // The callback receives the value as the first argument and key as second
8849 let call_args = match param_count {
8850 1 => vec![value.clone()],
8851 _ => vec![value.clone(), JValue::string(key.clone())],
8852 };
8853
8854 let fn_result = self.apply_function(func_arg, &call_args, data)?;
8855 // Skip undefined results (similar to map behavior)
8856 if !fn_result.is_null() && !fn_result.is_undefined() {
8857 result.push(fn_result);
8858 }
8859 }
8860 check_sequence_length(result.len(), &self.options)?;
8861 Ok(JValue::array(result))
8862 }
8863 JValue::Null => Ok(JValue::Null),
8864 _ => Err(EvaluatorError::TypeError(
8865 "each() first argument must be an object".to_string(),
8866 )),
8867 }
8868 }
8869
8870 "not" => {
8871 if evaluated_args.len() != 1 {
8872 return Err(EvaluatorError::EvaluationError(
8873 "not() requires exactly 1 argument".to_string(),
8874 ));
8875 }
8876 // $not(x) returns the logical negation of x
8877 // null is falsy, so $not(null) = true; undefined stays undefined
8878 if evaluated_args[0].is_undefined() {
8879 return Ok(JValue::Undefined);
8880 }
8881 Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
8882 }
8883 "boolean" => {
8884 if evaluated_args.len() != 1 {
8885 return Err(EvaluatorError::EvaluationError(
8886 "boolean() requires exactly 1 argument".to_string(),
8887 ));
8888 }
8889 if evaluated_args[0].is_undefined() {
8890 return Ok(JValue::Undefined);
8891 }
8892 Ok(functions::boolean::boolean(&evaluated_args[0])?)
8893 }
8894 "type" => {
8895 if evaluated_args.len() != 1 {
8896 return Err(EvaluatorError::EvaluationError(
8897 "type() requires exactly 1 argument".to_string(),
8898 ));
8899 }
8900 // Return type string
8901 // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
8902 // We use a special marker object to distinguish undefined from null
8903 match &evaluated_args[0] {
8904 JValue::Null => Ok(JValue::string("null")),
8905 JValue::Bool(_) => Ok(JValue::string("boolean")),
8906 JValue::Number(_) => Ok(JValue::string("number")),
8907 JValue::String(_) => Ok(JValue::string("string")),
8908 JValue::Array(_) => Ok(JValue::string("array")),
8909 JValue::Object(_) => Ok(JValue::string("object")),
8910 JValue::Undefined => Ok(JValue::Undefined),
8911 JValue::Lambda { .. } | JValue::Builtin { .. } => {
8912 Ok(JValue::string("function"))
8913 }
8914 JValue::Regex { .. } => Ok(JValue::string("regex")),
8915 }
8916 }
8917
8918 "base64encode" => {
8919 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8920 return Ok(JValue::Null);
8921 }
8922 if evaluated_args.len() != 1 {
8923 return Err(EvaluatorError::EvaluationError(
8924 "base64encode() requires exactly 1 argument".to_string(),
8925 ));
8926 }
8927 match &evaluated_args[0] {
8928 JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
8929 _ => Err(EvaluatorError::TypeError(
8930 "base64encode() requires a string argument".to_string(),
8931 )),
8932 }
8933 }
8934 "base64decode" => {
8935 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8936 return Ok(JValue::Null);
8937 }
8938 if evaluated_args.len() != 1 {
8939 return Err(EvaluatorError::EvaluationError(
8940 "base64decode() requires exactly 1 argument".to_string(),
8941 ));
8942 }
8943 match &evaluated_args[0] {
8944 JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
8945 _ => Err(EvaluatorError::TypeError(
8946 "base64decode() requires a string argument".to_string(),
8947 )),
8948 }
8949 }
8950 "encodeUrlComponent" => {
8951 if evaluated_args.len() != 1 {
8952 return Err(EvaluatorError::EvaluationError(
8953 "encodeUrlComponent() requires exactly 1 argument".to_string(),
8954 ));
8955 }
8956 if evaluated_args[0].is_null() {
8957 return Ok(JValue::Null);
8958 }
8959 if evaluated_args[0].is_undefined() {
8960 return Ok(JValue::Undefined);
8961 }
8962 match &evaluated_args[0] {
8963 JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
8964 _ => Err(EvaluatorError::TypeError(
8965 "encodeUrlComponent() requires a string argument".to_string(),
8966 )),
8967 }
8968 }
8969 "decodeUrlComponent" => {
8970 if evaluated_args.len() != 1 {
8971 return Err(EvaluatorError::EvaluationError(
8972 "decodeUrlComponent() requires exactly 1 argument".to_string(),
8973 ));
8974 }
8975 if evaluated_args[0].is_null() {
8976 return Ok(JValue::Null);
8977 }
8978 if evaluated_args[0].is_undefined() {
8979 return Ok(JValue::Undefined);
8980 }
8981 match &evaluated_args[0] {
8982 JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
8983 _ => Err(EvaluatorError::TypeError(
8984 "decodeUrlComponent() requires a string argument".to_string(),
8985 )),
8986 }
8987 }
8988 "encodeUrl" => {
8989 if evaluated_args.len() != 1 {
8990 return Err(EvaluatorError::EvaluationError(
8991 "encodeUrl() requires exactly 1 argument".to_string(),
8992 ));
8993 }
8994 if evaluated_args[0].is_null() {
8995 return Ok(JValue::Null);
8996 }
8997 if evaluated_args[0].is_undefined() {
8998 return Ok(JValue::Undefined);
8999 }
9000 match &evaluated_args[0] {
9001 JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
9002 _ => Err(EvaluatorError::TypeError(
9003 "encodeUrl() requires a string argument".to_string(),
9004 )),
9005 }
9006 }
9007 "decodeUrl" => {
9008 if evaluated_args.len() != 1 {
9009 return Err(EvaluatorError::EvaluationError(
9010 "decodeUrl() requires exactly 1 argument".to_string(),
9011 ));
9012 }
9013 if evaluated_args[0].is_null() {
9014 return Ok(JValue::Null);
9015 }
9016 if evaluated_args[0].is_undefined() {
9017 return Ok(JValue::Undefined);
9018 }
9019 match &evaluated_args[0] {
9020 JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
9021 _ => Err(EvaluatorError::TypeError(
9022 "decodeUrl() requires a string argument".to_string(),
9023 )),
9024 }
9025 }
9026
9027 "error" => {
9028 // $error(message) - throw error with custom message
9029 if evaluated_args.is_empty() {
9030 // No message provided
9031 return Err(EvaluatorError::EvaluationError(
9032 "D3137: $error() function evaluated".to_string(),
9033 ));
9034 }
9035
9036 match &evaluated_args[0] {
9037 JValue::String(s) => {
9038 Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
9039 }
9040 _ => Err(EvaluatorError::TypeError(
9041 "T0410: Argument 1 of function error does not match function signature"
9042 .to_string(),
9043 )),
9044 }
9045 }
9046 "assert" => {
9047 // $assert(condition, message) - throw error if condition is false
9048 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9049 return Err(EvaluatorError::EvaluationError(
9050 "assert() requires 1 or 2 arguments".to_string(),
9051 ));
9052 }
9053
9054 // First argument must be a boolean
9055 let condition = match &evaluated_args[0] {
9056 JValue::Bool(b) => *b,
9057 _ => {
9058 return Err(EvaluatorError::TypeError(
9059 "T0410: Argument 1 of function $assert does not match function signature".to_string(),
9060 ));
9061 }
9062 };
9063
9064 if !condition {
9065 let message = if evaluated_args.len() == 2 {
9066 match &evaluated_args[1] {
9067 JValue::String(s) => s.clone(),
9068 _ => Rc::from("$assert() statement failed"),
9069 }
9070 } else {
9071 Rc::from("$assert() statement failed")
9072 };
9073 return Err(EvaluatorError::EvaluationError(format!(
9074 "D3141: {}",
9075 message
9076 )));
9077 }
9078
9079 Ok(JValue::Null)
9080 }
9081
9082 "eval" => {
9083 // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
9084 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9085 return Err(EvaluatorError::EvaluationError(
9086 "T0410: Argument 1 of function $eval must be a string".to_string(),
9087 ));
9088 }
9089
9090 // If the first argument is null/undefined, return undefined
9091 if evaluated_args[0].is_null() {
9092 return Ok(JValue::Null);
9093 }
9094 if evaluated_args[0].is_undefined() {
9095 return Ok(JValue::Undefined);
9096 }
9097
9098 // First argument must be a string expression
9099 let expr_str = match &evaluated_args[0] {
9100 JValue::String(s) => &**s,
9101 _ => {
9102 return Err(EvaluatorError::EvaluationError(
9103 "T0410: Argument 1 of function $eval must be a string".to_string(),
9104 ));
9105 }
9106 };
9107
9108 // Parse the expression
9109 let parsed_ast = match parser::parse(expr_str) {
9110 Ok(ast) => ast,
9111 Err(e) => {
9112 // D3120 is the error code for parse errors in $eval
9113 return Err(EvaluatorError::EvaluationError(format!(
9114 "D3120: The expression passed to $eval cannot be parsed: {}",
9115 e
9116 )));
9117 }
9118 };
9119
9120 // Determine the context to use for evaluation
9121 let eval_context = if evaluated_args.len() == 2 {
9122 &evaluated_args[1]
9123 } else {
9124 data
9125 };
9126
9127 // Evaluate the parsed expression
9128 match self.evaluate_internal(&parsed_ast, eval_context) {
9129 Ok(result) => Ok(result),
9130 Err(e) => {
9131 // D3121 is the error code for evaluation errors in $eval
9132 let err_msg = e.to_string();
9133 if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
9134 Err(EvaluatorError::EvaluationError(format!(
9135 "D3121: {}",
9136 err_msg
9137 )))
9138 } else {
9139 Err(e)
9140 }
9141 }
9142 }
9143 }
9144
9145 "now" => {
9146 if !evaluated_args.is_empty() {
9147 return Err(EvaluatorError::EvaluationError(
9148 "now() takes no arguments".to_string(),
9149 ));
9150 }
9151 Ok(crate::datetime::now())
9152 }
9153
9154 "millis" => {
9155 if !evaluated_args.is_empty() {
9156 return Err(EvaluatorError::EvaluationError(
9157 "millis() takes no arguments".to_string(),
9158 ));
9159 }
9160 Ok(crate::datetime::millis())
9161 }
9162
9163 "toMillis" => {
9164 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9165 return Err(EvaluatorError::EvaluationError(
9166 "toMillis() requires 1 or 2 arguments".to_string(),
9167 ));
9168 }
9169
9170 match &evaluated_args[0] {
9171 JValue::String(s) => {
9172 // Optional second argument is a picture string for custom parsing
9173 if evaluated_args.len() == 2 {
9174 match &evaluated_args[1] {
9175 JValue::String(picture) => {
9176 // Use custom picture format parsing
9177 Ok(crate::datetime::to_millis_with_picture(s, picture)?)
9178 }
9179 JValue::Null => Ok(JValue::Null),
9180 JValue::Undefined => Ok(JValue::Undefined),
9181 _ => Err(EvaluatorError::TypeError(
9182 "toMillis() second argument must be a string".to_string(),
9183 )),
9184 }
9185 } else {
9186 // Use ISO 8601 partial date parsing
9187 Ok(crate::datetime::to_millis(s)?)
9188 }
9189 }
9190 JValue::Null => Ok(JValue::Null),
9191 JValue::Undefined => Ok(JValue::Undefined),
9192 _ => Err(EvaluatorError::TypeError(
9193 "toMillis() requires a string argument".to_string(),
9194 )),
9195 }
9196 }
9197
9198 "fromMillis" => {
9199 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
9200 return Err(EvaluatorError::EvaluationError(
9201 "fromMillis() requires 1 to 3 arguments".to_string(),
9202 ));
9203 }
9204
9205 match &evaluated_args[0] {
9206 JValue::Number(n) => {
9207 let millis = (if n.fract() == 0.0 {
9208 Ok(*n as i64)
9209 } else {
9210 Err(())
9211 })
9212 .map_err(|_| {
9213 EvaluatorError::TypeError(
9214 "fromMillis() requires an integer".to_string(),
9215 )
9216 })?;
9217
9218 let picture = match evaluated_args.get(1) {
9219 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9220 Some(JValue::String(s)) => Some(s.to_string()),
9221 Some(_) => {
9222 return Err(EvaluatorError::TypeError(
9223 "fromMillis() second argument must be a string".to_string(),
9224 ))
9225 }
9226 };
9227 let timezone = match evaluated_args.get(2) {
9228 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9229 Some(JValue::String(s)) => Some(s.to_string()),
9230 Some(_) => {
9231 return Err(EvaluatorError::TypeError(
9232 "fromMillis() third argument must be a string".to_string(),
9233 ))
9234 }
9235 };
9236
9237 Ok(crate::datetime::from_millis_with_picture(
9238 millis,
9239 picture.as_deref(),
9240 timezone.as_deref(),
9241 )?)
9242 }
9243 JValue::Null => Ok(JValue::Null),
9244 JValue::Undefined => Ok(JValue::Undefined),
9245 _ => Err(EvaluatorError::TypeError(
9246 "fromMillis() requires a number argument".to_string(),
9247 )),
9248 }
9249 }
9250
9251 _ => Err(EvaluatorError::ReferenceError(format!(
9252 "Unknown function: {}",
9253 name
9254 ))),
9255 }
9256 }
9257
9258 /// Apply a function (lambda or expression) to values
9259 ///
9260 /// This handles both:
9261 /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
9262 /// 2. Simple expressions: price * 2 - evaluates with values as context
9263 fn apply_function(
9264 &mut self,
9265 func_node: &AstNode,
9266 values: &[JValue],
9267 data: &JValue,
9268 ) -> Result<JValue, EvaluatorError> {
9269 match func_node {
9270 AstNode::Lambda {
9271 params,
9272 body,
9273 signature,
9274 thunk,
9275 } => {
9276 // Direct lambda - invoke it
9277 self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
9278 }
9279 AstNode::Function {
9280 name,
9281 args,
9282 is_builtin,
9283 } => {
9284 // Function call - check if it has placeholders (partial application)
9285 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
9286
9287 if has_placeholder {
9288 // This is a partial application - evaluate it to get the lambda value
9289 let partial_lambda =
9290 self.create_partial_application(name, args, *is_builtin, data)?;
9291
9292 // Now invoke the partial lambda with the provided values
9293 if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
9294 return self.invoke_stored_lambda(&stored, values, data);
9295 }
9296 Err(EvaluatorError::EvaluationError(
9297 "Failed to apply partial application".to_string(),
9298 ))
9299 } else {
9300 // Regular function call without placeholders
9301 // Evaluate it and apply if it returns a function
9302 let result = self.evaluate_internal(func_node, data)?;
9303
9304 // Check if result is a lambda value
9305 if let Some(stored) = self.lookup_lambda_from_value(&result) {
9306 return self.invoke_stored_lambda(&stored, values, data);
9307 }
9308
9309 // Otherwise just return the result
9310 Ok(result)
9311 }
9312 }
9313 AstNode::Variable(var_name) => {
9314 // Check if this variable holds a stored lambda
9315 if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
9316 self.invoke_stored_lambda(&stored_lambda, values, data)
9317 } else if let Some(value) = self.context.lookup(var_name).cloned() {
9318 // Check if this variable holds a lambda value
9319 // This handles lambdas passed as bound arguments in partial applications
9320 if let Some(stored) = self.lookup_lambda_from_value(&value) {
9321 return self.invoke_stored_lambda(&stored, values, data);
9322 }
9323 // Regular variable value - evaluate with first value as context
9324 if values.is_empty() {
9325 self.evaluate_internal(func_node, data)
9326 } else {
9327 self.evaluate_internal(func_node, &values[0])
9328 }
9329 } else if self.is_builtin_function(var_name) {
9330 // This is a built-in function reference (e.g., $string, $number)
9331 // Call it directly with the provided values (already evaluated)
9332 self.call_builtin_with_values(var_name, values)
9333 } else {
9334 // Unknown variable - evaluate with first value as context
9335 if values.is_empty() {
9336 self.evaluate_internal(func_node, data)
9337 } else {
9338 self.evaluate_internal(func_node, &values[0])
9339 }
9340 }
9341 }
9342 _ => {
9343 // For non-lambda expressions, evaluate with first value as context
9344 if values.is_empty() {
9345 self.evaluate_internal(func_node, data)
9346 } else {
9347 self.evaluate_internal(func_node, &values[0])
9348 }
9349 }
9350 }
9351 }
9352
9353 /// Execute a transform operator on the bound $ value
9354 fn execute_transform(
9355 &mut self,
9356 location: &AstNode,
9357 update: &AstNode,
9358 delete: Option<&AstNode>,
9359 _original_data: &JValue,
9360 ) -> Result<JValue, EvaluatorError> {
9361 // Get the input value from $ binding
9362 let input = self
9363 .context
9364 .lookup("$")
9365 .ok_or_else(|| {
9366 EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
9367 })?
9368 .clone();
9369
9370 // Evaluate location expression on the input to get objects to transform
9371 let located_objects = self.evaluate_internal(location, &input)?;
9372
9373 // Collect target objects into a vector for comparison
9374 let targets: Vec<JValue> = match located_objects {
9375 JValue::Array(arr) => arr.to_vec(),
9376 JValue::Object(_) => vec![located_objects],
9377 JValue::Null => Vec::new(),
9378 other => vec![other],
9379 };
9380
9381 // Validate update parameter - must be an object constructor
9382 // We need to check this before evaluation in case of errors
9383 // For now, we'll validate after evaluation in the transform helper
9384
9385 // Parse delete field names if provided
9386 let delete_fields: Vec<String> = if let Some(delete_node) = delete {
9387 let delete_val = self.evaluate_internal(delete_node, &input)?;
9388 match delete_val {
9389 JValue::Array(arr) => arr
9390 .iter()
9391 .filter_map(|v| match v {
9392 JValue::String(s) => Some(s.to_string()),
9393 _ => None,
9394 })
9395 .collect(),
9396 JValue::String(s) => vec![s.to_string()],
9397 JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
9398 _ => {
9399 // Delete parameter must be an array of strings or a string
9400 return Err(EvaluatorError::EvaluationError(
9401 "T2012: The third argument of the transform operator must be an array of strings".to_string()
9402 ));
9403 }
9404 }
9405 } else {
9406 Vec::new()
9407 };
9408
9409 // Recursive helper to apply transformation throughout the structure
9410 fn apply_transform_deep(
9411 evaluator: &mut Evaluator,
9412 value: &JValue,
9413 targets: &[JValue],
9414 update: &AstNode,
9415 delete_fields: &[String],
9416 ) -> Result<JValue, EvaluatorError> {
9417 // Check if this value is one of the targets to transform
9418 // Use JValue's PartialEq for semantic equality comparison
9419 if targets.iter().any(|t| t == value) {
9420 // Transform this object
9421 if let JValue::Object(map_rc) = value.clone() {
9422 let mut map = (*map_rc).clone();
9423 let update_val = evaluator.evaluate_internal(update, value)?;
9424 // Validate that update evaluates to an object or null (undefined)
9425 match update_val {
9426 JValue::Object(update_map) => {
9427 for (key, val) in update_map.iter() {
9428 map.insert(key.clone(), val.clone());
9429 }
9430 }
9431 JValue::Null | JValue::Undefined => {
9432 // Null/undefined means no updates, just continue to deletions
9433 }
9434 _ => {
9435 return Err(EvaluatorError::EvaluationError(
9436 "T2011: The second argument of the transform operator must evaluate to an object".to_string()
9437 ));
9438 }
9439 }
9440 for field in delete_fields {
9441 map.shift_remove(field);
9442 }
9443 return Ok(JValue::object(map));
9444 }
9445 return Ok(value.clone());
9446 }
9447
9448 // Otherwise, recursively process children to find and transform targets
9449 match value {
9450 JValue::Object(map) => {
9451 let mut new_map = IndexMap::new();
9452 for (k, v) in map.iter() {
9453 new_map.insert(
9454 k.clone(),
9455 apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
9456 );
9457 }
9458 Ok(JValue::object(new_map))
9459 }
9460 JValue::Array(arr) => {
9461 let mut new_arr = Vec::new();
9462 for item in arr.iter() {
9463 new_arr.push(apply_transform_deep(
9464 evaluator,
9465 item,
9466 targets,
9467 update,
9468 delete_fields,
9469 )?);
9470 }
9471 Ok(JValue::array(new_arr))
9472 }
9473 _ => Ok(value.clone()),
9474 }
9475 }
9476
9477 // Apply transformation recursively starting from input
9478 apply_transform_deep(self, &input, &targets, update, &delete_fields)
9479 }
9480
9481 /// Helper to invoke a lambda with given parameters
9482 fn invoke_lambda(
9483 &mut self,
9484 params: &[String],
9485 body: &AstNode,
9486 signature: Option<&String>,
9487 values: &[JValue],
9488 data: &JValue,
9489 thunk: bool,
9490 ) -> Result<JValue, EvaluatorError> {
9491 self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
9492 }
9493
9494 /// Invoke a lambda with optional captured environment (for closures)
9495 fn invoke_lambda_with_env(
9496 &mut self,
9497 params: &[String],
9498 body: &AstNode,
9499 signature: Option<&String>,
9500 values: &[JValue],
9501 data: &JValue,
9502 captured_env: Option<&HashMap<String, JValue>>,
9503 captured_data: Option<&JValue>,
9504 thunk: bool,
9505 ) -> Result<JValue, EvaluatorError> {
9506 // If this is a thunk (has tail calls), use TCO trampoline
9507 if thunk {
9508 let stored = StoredLambda {
9509 params: params.to_vec(),
9510 body: body.clone(),
9511 compiled_body: None, // Thunks use TCO, not the compiled fast path
9512 signature: signature.cloned(),
9513 captured_env: captured_env.cloned().unwrap_or_default(),
9514 captured_data: captured_data.cloned(),
9515 thunk,
9516 };
9517 return self.invoke_lambda_with_tco(&stored, values, data);
9518 }
9519
9520 // Validate signature if present, and get coerced arguments
9521 // Push a new scope for this lambda invocation
9522 self.context.push_scope();
9523
9524 // First apply captured environment (for closures)
9525 if let Some(env) = captured_env {
9526 for (name, value) in env {
9527 self.context.bind(name.clone(), value.clone());
9528 }
9529 }
9530
9531 if let Some(sig_str) = signature {
9532 // Validate and coerce arguments with signature
9533 let coerced_values = match crate::signature::Signature::parse(sig_str) {
9534 Ok(sig) => match sig.validate_and_coerce(values, data) {
9535 Ok(coerced) => coerced,
9536 Err(e) => {
9537 self.context.pop_scope();
9538 match e {
9539 crate::signature::SignatureError::UndefinedArgument => {
9540 return Ok(JValue::Null);
9541 }
9542 crate::signature::SignatureError::ArgumentTypeMismatch {
9543 index,
9544 expected,
9545 } => {
9546 return Err(EvaluatorError::TypeError(
9547 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9548 ));
9549 }
9550 crate::signature::SignatureError::ArrayTypeMismatch {
9551 index,
9552 expected,
9553 } => {
9554 return Err(EvaluatorError::TypeError(format!(
9555 "T0412: Argument {} of function must be an array of {}",
9556 index, expected
9557 )));
9558 }
9559 crate::signature::SignatureError::ContextTypeMismatch {
9560 index,
9561 expected,
9562 } => {
9563 return Err(EvaluatorError::TypeError(format!(
9564 "T0411: Context value at argument {} does not match function signature (expected {})",
9565 index, expected
9566 )));
9567 }
9568 _ => {
9569 return Err(EvaluatorError::TypeError(format!(
9570 "Signature validation failed: {}",
9571 e
9572 )));
9573 }
9574 }
9575 }
9576 },
9577 Err(e) => {
9578 self.context.pop_scope();
9579 return Err(EvaluatorError::EvaluationError(format!(
9580 "Invalid signature: {}",
9581 e
9582 )));
9583 }
9584 };
9585 // Bind coerced values to params
9586 for (i, param) in params.iter().enumerate() {
9587 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9588 self.context.bind(param.clone(), value);
9589 }
9590 } else {
9591 // No signature - bind directly from values slice (no allocation)
9592 for (i, param) in params.iter().enumerate() {
9593 let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9594 self.context.bind(param.clone(), value);
9595 }
9596 }
9597
9598 // Check if this is a partial application (body is a special marker string)
9599 if let AstNode::String(body_str) = body {
9600 if body_str.starts_with("__partial_call:") {
9601 // Parse the partial call info
9602 let parts: Vec<&str> = body_str.split(':').collect();
9603 if parts.len() >= 4 {
9604 let func_name = parts[1];
9605 let is_builtin = parts[2] == "true";
9606 let total_args: usize = parts[3].parse().unwrap_or(0);
9607
9608 // Get placeholder positions from captured env
9609 let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
9610 if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
9611 positions
9612 .iter()
9613 .filter_map(|v| v.as_f64().map(|n| n as usize))
9614 .collect()
9615 } else {
9616 vec![]
9617 }
9618 } else {
9619 vec![]
9620 };
9621
9622 // Reconstruct the full argument list
9623 let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
9624
9625 // Fill in bound arguments from captured environment
9626 if let Some(env) = captured_env {
9627 for (key, value) in env {
9628 if key.starts_with("__bound_arg_") {
9629 if let Ok(pos) = key[12..].parse::<usize>() {
9630 if pos < total_args {
9631 full_args[pos] = value.clone();
9632 }
9633 }
9634 }
9635 }
9636 }
9637
9638 // Fill in placeholder positions with provided values
9639 for (i, &pos) in placeholder_positions.iter().enumerate() {
9640 if pos < total_args {
9641 let value = values.get(i).cloned().unwrap_or(JValue::Null);
9642 full_args[pos] = value;
9643 }
9644 }
9645
9646 // Pop lambda scope, then push a new scope for temp args
9647 self.context.pop_scope();
9648 self.context.push_scope();
9649
9650 // Build AST nodes for the function call arguments
9651 let mut temp_args: Vec<AstNode> = Vec::new();
9652 for (i, value) in full_args.iter().enumerate() {
9653 let temp_name = format!("__temp_arg_{}", i);
9654 self.context.bind(temp_name.clone(), value.clone());
9655 temp_args.push(AstNode::Variable(temp_name));
9656 }
9657
9658 // Call the original function
9659 let result =
9660 self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
9661
9662 // Pop temp scope
9663 self.context.pop_scope();
9664
9665 return result;
9666 }
9667 }
9668 }
9669
9670 // Evaluate lambda body (normal case)
9671 // Use captured_data for lexical scoping if available, otherwise use call-site data
9672 let body_data = captured_data.unwrap_or(data);
9673 let result = self.evaluate_internal(body, body_data)?;
9674
9675 // Pop lambda scope, preserving any lambdas referenced by the return value
9676 // Fast path: scalar results can never contain lambda references
9677 let is_scalar = matches!(
9678 &result,
9679 JValue::Number(_)
9680 | JValue::Bool(_)
9681 | JValue::String(_)
9682 | JValue::Null
9683 | JValue::Undefined
9684 );
9685 if is_scalar {
9686 self.context.pop_scope();
9687 } else {
9688 let lambdas_to_keep = self.extract_lambda_ids(&result);
9689 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9690 }
9691
9692 Ok(result)
9693 }
9694
9695 /// Invoke a lambda with tail call optimization using a trampoline
9696 /// This method uses an iterative loop to handle tail-recursive calls without
9697 /// growing the stack, enabling deep recursion for tail-recursive functions.
9698 fn invoke_lambda_with_tco(
9699 &mut self,
9700 stored_lambda: &StoredLambda,
9701 initial_args: &[JValue],
9702 data: &JValue,
9703 ) -> Result<JValue, EvaluatorError> {
9704 let mut current_lambda = stored_lambda.clone();
9705 let mut current_args = initial_args.to_vec();
9706 let mut current_data = data.clone();
9707
9708 // Maximum number of tail call iterations to prevent infinite loops
9709 // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
9710 const MAX_TCO_ITERATIONS: usize = 100_000;
9711 let mut iterations = 0;
9712
9713 // Push a persistent scope for the TCO trampoline loop.
9714 // This scope persists across all iterations so that lambdas defined
9715 // in one iteration (like recursive $iter) remain available in subsequent ones.
9716 self.context.push_scope();
9717
9718 // Trampoline loop - keeps evaluating until we get a final value
9719 let result = loop {
9720 iterations += 1;
9721 // The hardcoded iteration cap is a backstop for when no timeout is
9722 // configured; it must not preempt a configured timeout (which is the
9723 // more specific, user-controlled guardrail). Without this gate, an
9724 // infinite tail-recursive loop with a cheap per-iteration body hits
9725 // this cap in single-digit-to-tens of milliseconds and reports the
9726 // misleading "U1001: Stack overflow" (TCO does not grow the stack;
9727 // there is no depth-500 stack here) instead of D1012, for *any*
9728 // realistic `timeout_ms` (100ms, 1s, the docs' own 5000ms default) -
9729 // defeating the purpose of the timeout guardrail for exactly the
9730 // scenario it exists to catch (see jsonata-js's own `$inf := function
9731 // (){$inf()}; $inf()` guardrails-documentation example).
9732 if self.options.timeout_ms.is_none() && iterations > MAX_TCO_ITERATIONS {
9733 self.context.pop_scope();
9734 return Err(EvaluatorError::EvaluationError(
9735 "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
9736 ));
9737 }
9738 if let Err(e) = check_loop_timeout(&self.options, self.start_time) {
9739 self.context.pop_scope();
9740 return Err(e);
9741 }
9742
9743 // Evaluate the lambda body within the persistent scope
9744 let result =
9745 self.invoke_lambda_body_for_tco(¤t_lambda, ¤t_args, ¤t_data)?;
9746
9747 match result {
9748 LambdaResult::JValue(v) => break v,
9749 LambdaResult::TailCall { lambda, args, data } => {
9750 // Continue with the tail call - no stack growth
9751 current_lambda = *lambda;
9752 current_args = args;
9753 current_data = data;
9754 }
9755 }
9756 };
9757
9758 // Pop the persistent TCO scope, preserving lambdas referenced by the result
9759 let lambdas_to_keep = self.extract_lambda_ids(&result);
9760 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9761
9762 Ok(result)
9763 }
9764
9765 /// Evaluate a lambda body, detecting tail calls for TCO
9766 /// Returns either a final value or a tail call continuation.
9767 /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
9768 /// manages the persistent scope for the trampoline loop.
9769 fn invoke_lambda_body_for_tco(
9770 &mut self,
9771 lambda: &StoredLambda,
9772 values: &[JValue],
9773 data: &JValue,
9774 ) -> Result<LambdaResult, EvaluatorError> {
9775 // Validate signature if present
9776 let coerced_values = if let Some(sig_str) = &lambda.signature {
9777 match crate::signature::Signature::parse(sig_str) {
9778 Ok(sig) => match sig.validate_and_coerce(values, data) {
9779 Ok(coerced) => coerced,
9780 Err(e) => match e {
9781 crate::signature::SignatureError::UndefinedArgument => {
9782 return Ok(LambdaResult::JValue(JValue::Null));
9783 }
9784 crate::signature::SignatureError::ArgumentTypeMismatch {
9785 index,
9786 expected,
9787 } => {
9788 return Err(EvaluatorError::TypeError(
9789 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9790 ));
9791 }
9792 crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
9793 return Err(EvaluatorError::TypeError(format!(
9794 "T0412: Argument {} of function must be an array of {}",
9795 index, expected
9796 )));
9797 }
9798 crate::signature::SignatureError::ContextTypeMismatch {
9799 index,
9800 expected,
9801 } => {
9802 return Err(EvaluatorError::TypeError(format!(
9803 "T0411: Context value at argument {} does not match function signature (expected {})",
9804 index, expected
9805 )));
9806 }
9807 _ => {
9808 return Err(EvaluatorError::TypeError(format!(
9809 "Signature validation failed: {}",
9810 e
9811 )));
9812 }
9813 },
9814 },
9815 Err(e) => {
9816 return Err(EvaluatorError::EvaluationError(format!(
9817 "Invalid signature: {}",
9818 e
9819 )));
9820 }
9821 }
9822 } else {
9823 values.to_vec()
9824 };
9825
9826 // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
9827 // Apply captured environment
9828 for (name, value) in &lambda.captured_env {
9829 self.context.bind(name.clone(), value.clone());
9830 }
9831
9832 // Bind parameters
9833 for (i, param) in lambda.params.iter().enumerate() {
9834 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
9835 self.context.bind(param.clone(), value);
9836 }
9837
9838 // Evaluate the body with tail call detection
9839 let body_data = lambda.captured_data.as_ref().unwrap_or(data);
9840 self.evaluate_for_tco(&lambda.body, body_data)
9841 }
9842
9843 /// Evaluate an expression for TCO, detecting tail calls
9844 /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
9845 fn evaluate_for_tco(
9846 &mut self,
9847 node: &AstNode,
9848 data: &JValue,
9849 ) -> Result<LambdaResult, EvaluatorError> {
9850 match node {
9851 // Conditional: evaluate condition, then evaluate the chosen branch for TCO
9852 AstNode::Conditional {
9853 condition,
9854 then_branch,
9855 else_branch,
9856 } => {
9857 let cond_value = self.evaluate_internal(condition, data)?;
9858 let is_truthy = self.is_truthy(&cond_value);
9859
9860 if is_truthy {
9861 self.evaluate_for_tco(then_branch, data)
9862 } else if let Some(else_expr) = else_branch {
9863 self.evaluate_for_tco(else_expr, data)
9864 } else {
9865 Ok(LambdaResult::JValue(JValue::Null))
9866 }
9867 }
9868
9869 // Block: evaluate all but last normally, last for TCO
9870 AstNode::Block(exprs) => {
9871 if exprs.is_empty() {
9872 return Ok(LambdaResult::JValue(JValue::Null));
9873 }
9874
9875 // Evaluate all expressions except the last
9876 let mut result = JValue::Null;
9877 for (i, expr) in exprs.iter().enumerate() {
9878 if i == exprs.len() - 1 {
9879 // Last expression - evaluate for TCO
9880 return self.evaluate_for_tco(expr, data);
9881 } else {
9882 result = self.evaluate_internal(expr, data)?;
9883 }
9884 }
9885 Ok(LambdaResult::JValue(result))
9886 }
9887
9888 // Variable binding: evaluate value, bind, then evaluate result for TCO if present
9889 AstNode::Binary {
9890 op: BinaryOp::ColonEqual,
9891 lhs,
9892 rhs,
9893 } => {
9894 // This is var := value; get the variable name
9895 let var_name = match lhs.as_ref() {
9896 AstNode::Variable(name) => name.clone(),
9897 _ => {
9898 // Not a simple variable binding, evaluate normally
9899 let result = self.evaluate_internal(node, data)?;
9900 return Ok(LambdaResult::JValue(result));
9901 }
9902 };
9903
9904 // Check if RHS is a lambda - store it specially
9905 if let AstNode::Lambda {
9906 params,
9907 body,
9908 signature,
9909 thunk,
9910 } = rhs.as_ref()
9911 {
9912 let captured_env = self.capture_environment_for(body, params);
9913 let compiled_body = if !thunk {
9914 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9915 try_compile_expr_with_allowed_vars(body, &var_refs)
9916 } else {
9917 None
9918 };
9919 let stored_lambda = StoredLambda {
9920 params: params.clone(),
9921 body: (**body).clone(),
9922 compiled_body,
9923 signature: signature.clone(),
9924 captured_env,
9925 captured_data: Some(data.clone()),
9926 thunk: *thunk,
9927 };
9928 self.context.bind_lambda(var_name, stored_lambda);
9929 let lambda_repr =
9930 JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
9931 return Ok(LambdaResult::JValue(lambda_repr));
9932 }
9933
9934 // Evaluate the RHS
9935 let value = self.evaluate_internal(rhs, data)?;
9936 self.context.bind(var_name, value.clone());
9937 Ok(LambdaResult::JValue(value))
9938 }
9939
9940 // Function call - this is where TCO happens
9941 AstNode::Function { name, args, .. } => {
9942 // Check if this is a call to a stored lambda (user function)
9943 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
9944 if stored_lambda.thunk {
9945 let mut evaluated_args = Vec::with_capacity(args.len());
9946 for arg in args {
9947 evaluated_args.push(self.evaluate_internal(arg, data)?);
9948 }
9949 return Ok(LambdaResult::TailCall {
9950 lambda: Box::new(stored_lambda),
9951 args: evaluated_args,
9952 data: data.clone(),
9953 });
9954 }
9955 }
9956 // Not a thunk lambda - evaluate normally
9957 let result = self.evaluate_internal(node, data)?;
9958 Ok(LambdaResult::JValue(result))
9959 }
9960
9961 // Call node (calling a lambda value)
9962 AstNode::Call { procedure, args } => {
9963 // Evaluate the procedure to get the callable
9964 let callable = self.evaluate_internal(procedure, data)?;
9965
9966 // Check if it's a lambda with TCO
9967 if let JValue::Lambda { lambda_id, .. } = &callable {
9968 if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
9969 if stored_lambda.thunk {
9970 let mut evaluated_args = Vec::with_capacity(args.len());
9971 for arg in args {
9972 evaluated_args.push(self.evaluate_internal(arg, data)?);
9973 }
9974 return Ok(LambdaResult::TailCall {
9975 lambda: Box::new(stored_lambda),
9976 args: evaluated_args,
9977 data: data.clone(),
9978 });
9979 }
9980 }
9981 }
9982 // Not a thunk - evaluate normally
9983 let result = self.evaluate_internal(node, data)?;
9984 Ok(LambdaResult::JValue(result))
9985 }
9986
9987 // Variable reference that might be a function call
9988 // This handles cases like $f($x) where $f is referenced by name
9989 AstNode::Variable(_) => {
9990 let result = self.evaluate_internal(node, data)?;
9991 Ok(LambdaResult::JValue(result))
9992 }
9993
9994 // Any other expression - evaluate normally
9995 _ => {
9996 let result = self.evaluate_internal(node, data)?;
9997 Ok(LambdaResult::JValue(result))
9998 }
9999 }
10000 }
10001
10002 /// Match with custom matcher function
10003 ///
10004 /// Implements custom matcher support for $match(str, matcherFunction, limit?)
10005 /// The matcher function is called with the string and returns:
10006 /// { match: string, start: number, end: number, groups: [], next: function }
10007 /// The next function is called repeatedly to get subsequent matches
10008 fn match_with_custom_matcher(
10009 &mut self,
10010 str_value: &str,
10011 matcher_node: &AstNode,
10012 limit: Option<usize>,
10013 data: &JValue,
10014 ) -> Result<JValue, EvaluatorError> {
10015 let mut results = Vec::new();
10016 let mut count = 0;
10017
10018 // Call the matcher function with the string
10019 let str_val = JValue::string(str_value.to_string());
10020 let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
10021
10022 // Iterate through matches following the 'next' chain
10023 while !current_match.is_undefined() && !current_match.is_null() {
10024 // Check limit
10025 if let Some(lim) = limit {
10026 if count >= lim {
10027 break;
10028 }
10029 }
10030
10031 // Extract match information from the result object
10032 if let JValue::Object(ref match_obj) = current_match {
10033 // Validate that this is a proper match object
10034 let has_match = match_obj.contains_key("match");
10035 let has_start = match_obj.contains_key("start");
10036 let has_end = match_obj.contains_key("end");
10037 let has_groups = match_obj.contains_key("groups");
10038 let has_next = match_obj.contains_key("next");
10039
10040 if !has_match && !has_start && !has_end && !has_groups && !has_next {
10041 // Invalid matcher result - T1010 error
10042 return Err(EvaluatorError::EvaluationError(
10043 "T1010: The matcher function did not return the correct object structure"
10044 .to_string(),
10045 ));
10046 }
10047
10048 // Build the result match object (match, index, groups)
10049 let mut result_obj = IndexMap::new();
10050
10051 if let Some(match_val) = match_obj.get("match") {
10052 result_obj.insert("match".to_string(), match_val.clone());
10053 }
10054
10055 if let Some(start_val) = match_obj.get("start") {
10056 result_obj.insert("index".to_string(), start_val.clone());
10057 }
10058
10059 if let Some(groups_val) = match_obj.get("groups") {
10060 result_obj.insert("groups".to_string(), groups_val.clone());
10061 }
10062
10063 results.push(JValue::object(result_obj));
10064 count += 1;
10065
10066 // Get the next match by calling the 'next' function
10067 if let Some(next_func) = match_obj.get("next") {
10068 if let Some(stored) = self.lookup_lambda_from_value(next_func) {
10069 current_match = self.invoke_stored_lambda(&stored, &[], data)?;
10070 continue;
10071 }
10072 }
10073
10074 // No next function or couldn't call it - stop iteration
10075 break;
10076 } else {
10077 // Not a valid match object
10078 break;
10079 }
10080 }
10081
10082 // Return results
10083 if results.is_empty() {
10084 Ok(JValue::Undefined)
10085 } else {
10086 Ok(JValue::array(results))
10087 }
10088 }
10089
10090 /// Replace with lambda/function callback
10091 ///
10092 /// Implements lambda replacement for $replace(str, pattern, function, limit?)
10093 /// The function receives a match object with: match, start, end, groups
10094 fn replace_with_lambda(
10095 &mut self,
10096 str_value: &JValue,
10097 pattern_value: &JValue,
10098 lambda_value: &JValue,
10099 limit_value: Option<&JValue>,
10100 data: &JValue,
10101 ) -> Result<JValue, EvaluatorError> {
10102 // Extract string
10103 let s = match str_value {
10104 JValue::String(s) => &**s,
10105 _ => {
10106 return Err(EvaluatorError::TypeError(
10107 "replace() requires string arguments".to_string(),
10108 ))
10109 }
10110 };
10111
10112 // Extract regex pattern
10113 let (pattern, flags) =
10114 crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
10115 EvaluatorError::TypeError(
10116 "replace() pattern must be a regex when using lambda replacement".to_string(),
10117 )
10118 })?;
10119
10120 // Build regex
10121 let re = crate::functions::string::build_regex(&pattern, &flags)?;
10122
10123 // Parse limit
10124 let limit = if let Some(lim_val) = limit_value {
10125 match lim_val {
10126 JValue::Number(n) => {
10127 let lim_f64 = *n;
10128 if lim_f64 < 0.0 {
10129 return Err(EvaluatorError::EvaluationError(format!(
10130 "D3011: Limit must be non-negative, got {}",
10131 lim_f64
10132 )));
10133 }
10134 Some(lim_f64 as usize)
10135 }
10136 _ => {
10137 return Err(EvaluatorError::TypeError(
10138 "replace() limit must be a number".to_string(),
10139 ))
10140 }
10141 }
10142 } else {
10143 None
10144 };
10145
10146 // Iterate through matches and replace using lambda
10147 let mut result = String::new();
10148 let mut last_end = 0;
10149 let mut count = 0;
10150
10151 for cap in re.captures_iter(s) {
10152 // Check limit
10153 if let Some(lim) = limit {
10154 if count >= lim {
10155 break;
10156 }
10157 }
10158
10159 let m = cap.get(0).unwrap();
10160 let match_start = m.start();
10161 let match_end = m.end();
10162 let match_str = m.as_str();
10163
10164 // Add text before match
10165 result.push_str(&s[last_end..match_start]);
10166
10167 // Build match object
10168 let groups: Vec<JValue> = (1..cap.len())
10169 .map(|i| {
10170 cap.get(i)
10171 .map(|m| JValue::string(m.as_str().to_string()))
10172 .unwrap_or(JValue::Null)
10173 })
10174 .collect();
10175
10176 let mut match_map = IndexMap::new();
10177 match_map.insert("match".to_string(), JValue::string(match_str));
10178 match_map.insert("start".to_string(), JValue::Number(match_start as f64));
10179 match_map.insert("end".to_string(), JValue::Number(match_end as f64));
10180 match_map.insert("groups".to_string(), JValue::array(groups));
10181 let match_obj = JValue::object(match_map);
10182
10183 // Invoke lambda with match object
10184 let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
10185 EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
10186 })?;
10187 let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
10188 let replacement_str = match lambda_result {
10189 JValue::String(s) => s,
10190 _ => {
10191 return Err(EvaluatorError::TypeError(format!(
10192 "D3012: Replacement function must return a string, got {:?}",
10193 lambda_result
10194 )))
10195 }
10196 };
10197
10198 // Add replacement
10199 result.push_str(&replacement_str);
10200
10201 last_end = match_end;
10202 count += 1;
10203 }
10204
10205 // Add remaining text after last match
10206 result.push_str(&s[last_end..]);
10207
10208 Ok(JValue::string(result))
10209 }
10210
10211 /// Capture the current environment bindings for closure support
10212 fn capture_current_environment(&self) -> HashMap<String, JValue> {
10213 self.context.all_bindings()
10214 }
10215
10216 /// Capture only the variables referenced by a lambda body (selective capture).
10217 /// This avoids cloning the entire environment when only a few variables are needed.
10218 fn capture_environment_for(
10219 &self,
10220 body: &AstNode,
10221 params: &[String],
10222 ) -> HashMap<String, JValue> {
10223 let free_vars = Self::collect_free_variables(body, params);
10224 if free_vars.is_empty() {
10225 return HashMap::new();
10226 }
10227 let mut result = HashMap::new();
10228 for var_name in &free_vars {
10229 if let Some(value) = self.context.lookup(var_name) {
10230 result.insert(var_name.clone(), value.clone());
10231 }
10232 }
10233 result
10234 }
10235
10236 /// Collect all free variables in an AST node that are not bound by the given params.
10237 /// A "free variable" is one that is referenced but not defined within the expression.
10238 fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
10239 let mut free_vars = HashSet::new();
10240 let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
10241 Self::collect_free_vars_walk(body, &bound, &mut free_vars);
10242 free_vars
10243 }
10244
10245 fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
10246 match node {
10247 AstNode::Variable(name) => {
10248 if !bound.contains(name.as_str()) {
10249 free.insert(name.clone());
10250 }
10251 }
10252 AstNode::Function { name, args, .. } => {
10253 // Function name references a variable (e.g., $f(...))
10254 if !bound.contains(name.as_str()) {
10255 free.insert(name.clone());
10256 }
10257 for arg in args {
10258 Self::collect_free_vars_walk(arg, bound, free);
10259 }
10260 }
10261 AstNode::Lambda { params, body, .. } => {
10262 // Inner lambda introduces new bindings
10263 let mut inner_bound = bound.clone();
10264 for p in params {
10265 inner_bound.insert(p.as_str());
10266 }
10267 Self::collect_free_vars_walk(body, &inner_bound, free);
10268 }
10269 AstNode::Binary { op, lhs, rhs } => {
10270 Self::collect_free_vars_walk(lhs, bound, free);
10271 Self::collect_free_vars_walk(rhs, bound, free);
10272 // For ColonEqual, note: the binding is visible after this expr in blocks,
10273 // but block handling takes care of that separately
10274 let _ = op;
10275 }
10276 AstNode::Unary { operand, .. } => {
10277 Self::collect_free_vars_walk(operand, bound, free);
10278 }
10279 AstNode::Path { steps } => {
10280 for step in steps {
10281 Self::collect_free_vars_walk(&step.node, bound, free);
10282 for stage in &step.stages {
10283 match stage {
10284 Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
10285 // An index stage binds a variable; it introduces no
10286 // free variable references.
10287 Stage::Index(_) => {}
10288 }
10289 }
10290 }
10291 }
10292 AstNode::Call { procedure, args } => {
10293 Self::collect_free_vars_walk(procedure, bound, free);
10294 for arg in args {
10295 Self::collect_free_vars_walk(arg, bound, free);
10296 }
10297 }
10298 AstNode::Conditional {
10299 condition,
10300 then_branch,
10301 else_branch,
10302 } => {
10303 Self::collect_free_vars_walk(condition, bound, free);
10304 Self::collect_free_vars_walk(then_branch, bound, free);
10305 if let Some(else_expr) = else_branch {
10306 Self::collect_free_vars_walk(else_expr, bound, free);
10307 }
10308 }
10309 AstNode::Block(exprs) => {
10310 let mut block_bound = bound.clone();
10311 for expr in exprs {
10312 Self::collect_free_vars_walk(expr, &block_bound, free);
10313 // Bindings introduced via := become bound for subsequent expressions
10314 if let AstNode::Binary {
10315 op: BinaryOp::ColonEqual,
10316 lhs,
10317 ..
10318 } = expr
10319 {
10320 if let AstNode::Variable(var_name) = lhs.as_ref() {
10321 block_bound.insert(var_name.as_str());
10322 }
10323 }
10324 }
10325 }
10326 AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
10327 for expr in exprs {
10328 Self::collect_free_vars_walk(expr, bound, free);
10329 }
10330 }
10331 AstNode::Object(pairs) => {
10332 for (key, value) in pairs {
10333 Self::collect_free_vars_walk(key, bound, free);
10334 Self::collect_free_vars_walk(value, bound, free);
10335 }
10336 }
10337 AstNode::ObjectTransform { input, pattern } => {
10338 Self::collect_free_vars_walk(input, bound, free);
10339 for (key, value) in pattern {
10340 Self::collect_free_vars_walk(key, bound, free);
10341 Self::collect_free_vars_walk(value, bound, free);
10342 }
10343 }
10344 AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
10345 Self::collect_free_vars_walk(expr, bound, free);
10346 }
10347 AstNode::Sort { input, terms } => {
10348 Self::collect_free_vars_walk(input, bound, free);
10349 for (expr, _) in terms {
10350 Self::collect_free_vars_walk(expr, bound, free);
10351 }
10352 }
10353 AstNode::Transform {
10354 location,
10355 update,
10356 delete,
10357 } => {
10358 Self::collect_free_vars_walk(location, bound, free);
10359 Self::collect_free_vars_walk(update, bound, free);
10360 if let Some(del) = delete {
10361 Self::collect_free_vars_walk(del, bound, free);
10362 }
10363 }
10364 // Leaf nodes with no variable references
10365 AstNode::String(_)
10366 | AstNode::Name(_)
10367 | AstNode::Number(_)
10368 | AstNode::Boolean(_)
10369 | AstNode::Null
10370 | AstNode::Undefined
10371 | AstNode::Placeholder
10372 | AstNode::Regex { .. }
10373 | AstNode::Wildcard
10374 | AstNode::Descendant
10375 | AstNode::Parent(_)
10376 | AstNode::ParentVariable(_) => {}
10377 }
10378 }
10379
10380 /// Check if a name refers to a built-in function
10381 fn is_builtin_function(&self, name: &str) -> bool {
10382 matches!(
10383 name,
10384 // String functions
10385 "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
10386 "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
10387 "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
10388 "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
10389
10390 // Numeric functions
10391 "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
10392 "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
10393
10394 // Aggregation functions
10395 "sum" | "max" | "min" | "average" |
10396
10397 // Boolean/logic functions
10398 "boolean" | "not" | "exists" |
10399
10400 // Array functions
10401 "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
10402
10403 // Object functions
10404 "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
10405
10406 // Higher-order functions
10407 "map" | "filter" | "reduce" | "singletonArray" |
10408
10409 // Date/time functions
10410 "now" | "millis" | "fromMillis" | "toMillis"
10411 )
10412 }
10413
10414 /// Call a built-in function directly with pre-evaluated Values
10415 /// This is used when passing built-in functions to higher-order functions like $map
10416 fn call_builtin_with_values(
10417 &mut self,
10418 name: &str,
10419 values: &[JValue],
10420 ) -> Result<JValue, EvaluatorError> {
10421 use crate::functions;
10422
10423 if values.is_empty() {
10424 return Err(EvaluatorError::EvaluationError(format!(
10425 "{}() requires at least 1 argument",
10426 name
10427 )));
10428 }
10429
10430 let arg = &values[0];
10431
10432 match name {
10433 "string" => Ok(functions::string::string(arg, None)?),
10434 "number" => Ok(functions::numeric::number(arg)?),
10435 "boolean" => Ok(functions::boolean::boolean(arg)?),
10436 "not" => {
10437 let b = functions::boolean::boolean(arg)?;
10438 match b {
10439 JValue::Bool(val) => Ok(JValue::Bool(!val)),
10440 _ => Err(EvaluatorError::TypeError(
10441 "not() requires a boolean".to_string(),
10442 )),
10443 }
10444 }
10445 "exists" => Ok(JValue::Bool(!arg.is_null())),
10446 "abs" => match arg {
10447 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
10448 _ => Err(EvaluatorError::TypeError(
10449 "abs() requires a number argument".to_string(),
10450 )),
10451 },
10452 "floor" => match arg {
10453 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
10454 _ => Err(EvaluatorError::TypeError(
10455 "floor() requires a number argument".to_string(),
10456 )),
10457 },
10458 "ceil" => match arg {
10459 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
10460 _ => Err(EvaluatorError::TypeError(
10461 "ceil() requires a number argument".to_string(),
10462 )),
10463 },
10464 "round" => match arg {
10465 JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
10466 _ => Err(EvaluatorError::TypeError(
10467 "round() requires a number argument".to_string(),
10468 )),
10469 },
10470 "sqrt" => match arg {
10471 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
10472 _ => Err(EvaluatorError::TypeError(
10473 "sqrt() requires a number argument".to_string(),
10474 )),
10475 },
10476 "uppercase" => match arg {
10477 JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
10478 JValue::Null => Ok(JValue::Null),
10479 _ => Err(EvaluatorError::TypeError(
10480 "uppercase() requires a string argument".to_string(),
10481 )),
10482 },
10483 "lowercase" => match arg {
10484 JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
10485 JValue::Null => Ok(JValue::Null),
10486 _ => Err(EvaluatorError::TypeError(
10487 "lowercase() requires a string argument".to_string(),
10488 )),
10489 },
10490 "trim" => match arg {
10491 JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
10492 JValue::Null => Ok(JValue::Null),
10493 _ => Err(EvaluatorError::TypeError(
10494 "trim() requires a string argument".to_string(),
10495 )),
10496 },
10497 "length" => match arg {
10498 JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
10499 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10500 JValue::Null => Ok(JValue::Null),
10501 _ => Err(EvaluatorError::TypeError(
10502 "length() requires a string or array argument".to_string(),
10503 )),
10504 },
10505 "sum" => match arg {
10506 JValue::Array(arr) => {
10507 let mut total = 0.0;
10508 for item in arr.iter() {
10509 match item {
10510 JValue::Number(n) => {
10511 total += *n;
10512 }
10513 _ => {
10514 return Err(EvaluatorError::TypeError(
10515 "sum() requires all array elements to be numbers".to_string(),
10516 ));
10517 }
10518 }
10519 }
10520 Ok(JValue::Number(total))
10521 }
10522 JValue::Number(n) => Ok(JValue::Number(*n)),
10523 JValue::Null => Ok(JValue::Null),
10524 _ => Err(EvaluatorError::TypeError(
10525 "sum() requires an array of numbers".to_string(),
10526 )),
10527 },
10528 "count" => {
10529 match arg {
10530 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10531 JValue::Null => Ok(JValue::Number(0.0)),
10532 _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10533 }
10534 }
10535 "max" => match arg {
10536 JValue::Array(arr) => {
10537 let mut max_val: Option<f64> = None;
10538 for item in arr.iter() {
10539 if let JValue::Number(n) = item {
10540 let f = *n;
10541 max_val = Some(max_val.map_or(f, |m| m.max(f)));
10542 }
10543 }
10544 max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10545 }
10546 JValue::Number(n) => Ok(JValue::Number(*n)),
10547 JValue::Null => Ok(JValue::Null),
10548 _ => Err(EvaluatorError::TypeError(
10549 "max() requires an array of numbers".to_string(),
10550 )),
10551 },
10552 "min" => match arg {
10553 JValue::Array(arr) => {
10554 let mut min_val: Option<f64> = None;
10555 for item in arr.iter() {
10556 if let JValue::Number(n) = item {
10557 let f = *n;
10558 min_val = Some(min_val.map_or(f, |m| m.min(f)));
10559 }
10560 }
10561 min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10562 }
10563 JValue::Number(n) => Ok(JValue::Number(*n)),
10564 JValue::Null => Ok(JValue::Null),
10565 _ => Err(EvaluatorError::TypeError(
10566 "min() requires an array of numbers".to_string(),
10567 )),
10568 },
10569 "average" => match arg {
10570 JValue::Array(arr) => {
10571 let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
10572 if nums.is_empty() {
10573 Ok(JValue::Null)
10574 } else {
10575 let avg = nums.iter().sum::<f64>() / nums.len() as f64;
10576 Ok(JValue::Number(avg))
10577 }
10578 }
10579 JValue::Number(n) => Ok(JValue::Number(*n)),
10580 JValue::Null => Ok(JValue::Null),
10581 _ => Err(EvaluatorError::TypeError(
10582 "average() requires an array of numbers".to_string(),
10583 )),
10584 },
10585 "append" => {
10586 // append(array1, array2) - append second array to first
10587 if values.len() < 2 {
10588 return Err(EvaluatorError::EvaluationError(
10589 "append() requires 2 arguments".to_string(),
10590 ));
10591 }
10592 let first = &values[0];
10593 let second = &values[1];
10594
10595 // Convert first to array if needed
10596 let mut result = match first {
10597 JValue::Array(arr) => arr.to_vec(),
10598 JValue::Null => vec![],
10599 other => vec![other.clone()],
10600 };
10601
10602 // Append second (flatten if array)
10603 match second {
10604 JValue::Array(arr) => result.extend(arr.iter().cloned()),
10605 JValue::Null => {}
10606 other => result.push(other.clone()),
10607 }
10608
10609 check_sequence_length(result.len(), &self.options)?;
10610 Ok(JValue::array(result))
10611 }
10612 "reverse" => match arg {
10613 JValue::Array(arr) => {
10614 let mut reversed = arr.to_vec();
10615 reversed.reverse();
10616 Ok(JValue::array(reversed))
10617 }
10618 JValue::Null => Ok(JValue::Null),
10619 _ => Err(EvaluatorError::TypeError(
10620 "reverse() requires an array".to_string(),
10621 )),
10622 },
10623 "keys" => match arg {
10624 JValue::Object(obj) => {
10625 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
10626 check_sequence_length(keys.len(), &self.options)?;
10627 Ok(JValue::array(keys))
10628 }
10629 JValue::Null => Ok(JValue::Null),
10630 _ => Err(EvaluatorError::TypeError(
10631 "keys() requires an object".to_string(),
10632 )),
10633 },
10634
10635 // Add more functions as needed
10636 _ => Err(EvaluatorError::ReferenceError(format!(
10637 "Built-in function {} cannot be called with values directly",
10638 name
10639 ))),
10640 }
10641 }
10642
10643 /// Collect all descendant values recursively
10644 fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
10645 let mut descendants = Vec::new();
10646
10647 match value {
10648 JValue::Null => {
10649 // Null has no descendants, return empty
10650 return descendants;
10651 }
10652 JValue::Object(obj) => {
10653 // Include the current object
10654 descendants.push(value.clone());
10655
10656 for val in obj.values() {
10657 // Recursively collect descendants
10658 descendants.extend(self.collect_descendants(val));
10659 }
10660 }
10661 JValue::Array(arr) => {
10662 // DO NOT include the array itself - only recurse into elements
10663 // This matches JavaScript behavior: arrays are traversed but not collected
10664 for val in arr.iter() {
10665 // Recursively collect descendants
10666 descendants.extend(self.collect_descendants(val));
10667 }
10668 }
10669 _ => {
10670 // For primitives (string, number, boolean), just include the value itself
10671 descendants.push(value.clone());
10672 }
10673 }
10674
10675 descendants
10676 }
10677
10678 /// Evaluate a predicate (array filter or index)
10679 fn evaluate_predicate(
10680 &mut self,
10681 current: &JValue,
10682 predicate: &AstNode,
10683 ) -> Result<JValue, EvaluatorError> {
10684 // Special case: empty brackets [] (represented as Boolean(true))
10685 // This forces the value to be wrapped in an array
10686 if matches!(predicate, AstNode::Boolean(true)) {
10687 return match current {
10688 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
10689 JValue::Null => Ok(JValue::Null),
10690 other => Ok(JValue::array(vec![other.clone()])),
10691 };
10692 }
10693
10694 match current {
10695 JValue::Array(_arr) => {
10696 // Standalone predicates do simple array operations (no mapping over sub-arrays)
10697
10698 // First, try to evaluate predicate as a simple number (array index)
10699 if let AstNode::Number(n) = predicate {
10700 // Direct array indexing
10701 return self.array_index(current, &JValue::Number(*n));
10702 }
10703
10704 // Fast path: if predicate is definitely a filter expression (comparison/logical),
10705 // skip speculative numeric evaluation and go directly to filter logic
10706 if Self::is_filter_predicate(predicate) {
10707 // Try CompiledExpr fast path
10708 if let Some(compiled) = try_compile_expr(predicate) {
10709 let shape = _arr.first().and_then(build_shape_cache);
10710 let mut filtered = Vec::with_capacity(_arr.len());
10711 for item in _arr.iter() {
10712 let result = if let Some(ref s) = shape {
10713 eval_compiled_shaped(
10714 &compiled,
10715 item,
10716 None,
10717 s,
10718 &self.options,
10719 self.start_time,
10720 )?
10721 } else {
10722 eval_compiled(
10723 &compiled,
10724 item,
10725 None,
10726 &self.options,
10727 self.start_time,
10728 )?
10729 };
10730 if compiled_is_truthy(&result) {
10731 filtered.push(item.clone());
10732 }
10733 }
10734 return Ok(JValue::array(filtered));
10735 }
10736 // Fallback: full AST evaluation per element
10737 let mut filtered = Vec::new();
10738 for item in _arr.iter() {
10739 let item_result = self.evaluate_internal(predicate, item)?;
10740 if self.is_truthy(&item_result) {
10741 filtered.push(item.clone());
10742 }
10743 }
10744 return Ok(JValue::array(filtered));
10745 }
10746
10747 // Try to evaluate the predicate to see if it's a numeric index
10748 // If evaluation succeeds and yields a number, use it as an index
10749 // If evaluation fails (e.g., comparison error), treat as filter
10750 match self.evaluate_internal(predicate, current) {
10751 Ok(JValue::Number(_)) => {
10752 // It's a numeric index
10753 let pred_result = self.evaluate_internal(predicate, current)?;
10754 return self.array_index(current, &pred_result);
10755 }
10756 Ok(JValue::Array(indices)) => {
10757 // Multiple array selectors [[indices]]
10758 // Check if array contains any non-numeric values
10759 let has_non_numeric =
10760 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
10761
10762 if has_non_numeric {
10763 // If array contains non-numeric values, return entire array
10764 return Ok(current.clone());
10765 }
10766
10767 // Collect numeric indices, handling negative indices
10768 let arr_len = _arr.len() as i64;
10769 let mut resolved_indices: Vec<i64> = indices
10770 .iter()
10771 .filter_map(|v| {
10772 if let JValue::Number(n) = v {
10773 let idx = *n as i64;
10774 // Resolve negative indices
10775 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
10776 // Only include valid indices
10777 if actual_idx >= 0 && actual_idx < arr_len {
10778 Some(actual_idx)
10779 } else {
10780 None
10781 }
10782 } else {
10783 None
10784 }
10785 })
10786 .collect();
10787
10788 // Sort and deduplicate indices
10789 resolved_indices.sort();
10790 resolved_indices.dedup();
10791
10792 // Select elements at each sorted index
10793 let result: Vec<JValue> = resolved_indices
10794 .iter()
10795 .map(|&idx| _arr[idx as usize].clone())
10796 .collect();
10797
10798 return Ok(JValue::array(result));
10799 }
10800 Ok(_) => {
10801 // Evaluated successfully but not a number - might be a filter
10802 // Fall through to filter logic
10803 }
10804 Err(_) => {
10805 // Evaluation failed - it's likely a filter expression
10806 // Fall through to filter logic
10807 }
10808 }
10809
10810 // Try CompiledExpr fast path for filter expressions
10811 if let Some(compiled) = try_compile_expr(predicate) {
10812 let shape = _arr.first().and_then(build_shape_cache);
10813 let mut filtered = Vec::with_capacity(_arr.len());
10814 for item in _arr.iter() {
10815 let result = if let Some(ref s) = shape {
10816 eval_compiled_shaped(
10817 &compiled,
10818 item,
10819 None,
10820 s,
10821 &self.options,
10822 self.start_time,
10823 )?
10824 } else {
10825 eval_compiled(&compiled, item, None, &self.options, self.start_time)?
10826 };
10827 if compiled_is_truthy(&result) {
10828 filtered.push(item.clone());
10829 }
10830 }
10831 return Ok(JValue::array(filtered));
10832 }
10833
10834 // It's a filter expression - evaluate the predicate for each array element
10835 let mut filtered = Vec::new();
10836 for item in _arr.iter() {
10837 let item_result = self.evaluate_internal(predicate, item)?;
10838
10839 // If result is truthy, include this item
10840 if self.is_truthy(&item_result) {
10841 filtered.push(item.clone());
10842 }
10843 }
10844
10845 Ok(JValue::array(filtered))
10846 }
10847 JValue::Object(obj) => {
10848 // For objects, predicate can be either:
10849 // 1. A string - property access (computed property name)
10850 // 2. A boolean expression - filter (return object if truthy)
10851 let pred_result = self.evaluate_internal(predicate, current)?;
10852
10853 // If it's a string, use it as a key for property access
10854 if let JValue::String(key) = &pred_result {
10855 return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
10856 }
10857
10858 // Otherwise, treat as a filter expression
10859 // If the predicate is truthy, return the object; otherwise return undefined
10860 if self.is_truthy(&pred_result) {
10861 Ok(current.clone())
10862 } else {
10863 Ok(JValue::Undefined)
10864 }
10865 }
10866 _ => {
10867 // For primitive values (string, number, boolean):
10868 // In JSONata, scalars are treated as single-element arrays when indexed.
10869 // So value[0] returns value, value[1] returns undefined.
10870
10871 // First check if predicate is a numeric literal
10872 if let AstNode::Number(n) = predicate {
10873 // For scalars, index 0 or -1 returns the value, others return undefined
10874 let idx = n.floor() as i64;
10875 if idx == 0 || idx == -1 {
10876 return Ok(current.clone());
10877 } else {
10878 return Ok(JValue::Undefined);
10879 }
10880 }
10881
10882 // Try to evaluate the predicate to see if it's a numeric index
10883 let pred_result = self.evaluate_internal(predicate, current)?;
10884
10885 if let JValue::Number(n) = &pred_result {
10886 // It's a numeric index - treat scalar as single-element array
10887 let idx = n.floor() as i64;
10888 if idx == 0 || idx == -1 {
10889 return Ok(current.clone());
10890 } else {
10891 return Ok(JValue::Undefined);
10892 }
10893 }
10894
10895 // For non-numeric predicates, treat as a filter:
10896 // value[true] returns value, value[false] returns undefined
10897 // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
10898 if self.is_truthy(&pred_result) {
10899 Ok(current.clone())
10900 } else {
10901 // Return undefined (not null) so $map can filter it out
10902 Ok(JValue::Undefined)
10903 }
10904 }
10905 }
10906 }
10907
10908 /// Evaluate a sort term expression, distinguishing missing fields from explicit null
10909 /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
10910 fn evaluate_sort_term(
10911 &mut self,
10912 term_expr: &AstNode,
10913 element: &JValue,
10914 ) -> Result<JValue, EvaluatorError> {
10915 // For tuples (from index binding), extract the actual value from @ field
10916 let actual_element = if let JValue::Object(obj) = element {
10917 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
10918 obj.get("@").cloned().unwrap_or(JValue::Null)
10919 } else {
10920 element.clone()
10921 }
10922 } else {
10923 element.clone()
10924 };
10925
10926 // For simple field access (Path with single Name step), check if field exists
10927 if let AstNode::Path { steps } = term_expr {
10928 if steps.len() == 1 && steps[0].stages.is_empty() {
10929 if let AstNode::Name(field_name) = &steps[0].node {
10930 // Check if the field exists in the element
10931 if let JValue::Object(obj) = &actual_element {
10932 return match obj.get(field_name) {
10933 Some(val) => Ok(val.clone()), // Field exists (may be null)
10934 None => Ok(JValue::Undefined), // Field is missing
10935 };
10936 } else {
10937 // Not an object - return undefined
10938 return Ok(JValue::Undefined);
10939 }
10940 }
10941 }
10942 }
10943
10944 // For complex expressions, evaluate against the tuple's `@` value (the
10945 // real element), not the wrapper. The tuple's carried focus/index/ancestor
10946 // bindings are reachable via context (bound by evaluate_sort), so a term
10947 // like `$`, `%.Price`, or `$pos` still resolves correctly.
10948 let result = self.evaluate_internal(term_expr, &actual_element)?;
10949
10950 // If the result is null from a complex expression, we can't easily tell if it's
10951 // "missing field" or "explicit null". For now, treat null results as undefined
10952 // to maintain compatibility with existing tests.
10953 // TODO: For full JS compatibility, would need deeper analysis of the expression
10954 if result.is_null() {
10955 return Ok(JValue::Undefined);
10956 }
10957
10958 Ok(result)
10959 }
10960
10961 /// Evaluate sort operator
10962 fn evaluate_sort(
10963 &mut self,
10964 data: &JValue,
10965 terms: &[(AstNode, bool)],
10966 ) -> Result<JValue, EvaluatorError> {
10967 // If data is null, return null
10968 if data.is_null() {
10969 return Ok(JValue::Null);
10970 }
10971
10972 // If data is not an array, return it as-is (can't sort a single value)
10973 let array = match data {
10974 JValue::Array(arr) => arr.clone(),
10975 other => return Ok(other.clone()),
10976 };
10977
10978 // If empty array, return as-is
10979 if array.is_empty() {
10980 return Ok(JValue::Array(array));
10981 }
10982
10983 // Evaluate sort keys for each element
10984 let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
10985
10986 for (idx, element) in array.iter().enumerate() {
10987 let mut sort_keys = Vec::new();
10988
10989 // When sorting a tuple stream (the input path had a `%`/`@`/`#`
10990 // step, so each element is a `{@, !label, $var, __tuple__}`
10991 // wrapper), bind its carried ancestor/focus/index keys into scope
10992 // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
10993 // create_tuple_stream's per-tuple frame binding. Sort terms attach
10994 // to a synthetic step after the last input step, so `%` refers to
10995 // the last input step's ancestry, carried under `!label` here.
10996 // Saves/restores rather than blindly unbinding, so a tuple key
10997 // that collides with a live outer `:=` binding doesn't get
10998 // deleted once this row's sort terms are evaluated.
10999 let tuple_bindings = match element {
11000 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11001 Some(self.bind_tuple_keys(obj))
11002 }
11003 _ => None,
11004 };
11005
11006 // When sorting a tuple stream, `$` and the term's data context are the
11007 // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
11008 // otherwise a term like `^($)` would try to order by the wrapper
11009 // object and raise T2008. The carried focus/index/ancestor keys stay
11010 // reachable via the context bindings established just above.
11011 let term_data = match element {
11012 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11013 obj.get("@").cloned().unwrap_or(JValue::Null)
11014 }
11015 other => other.clone(),
11016 };
11017
11018 // Evaluate each sort term with $ bound to the element
11019 for (term_expr, _ascending) in terms {
11020 // Save current $ binding
11021 let saved_dollar = self.context.lookup("$").cloned();
11022
11023 // Bind $ to current element
11024 self.context.bind("$".to_string(), term_data.clone());
11025
11026 // Evaluate the sort expression, distinguishing missing fields from explicit null
11027 let sort_value = self.evaluate_sort_term(term_expr, element)?;
11028
11029 // Restore $ binding
11030 if let Some(val) = saved_dollar {
11031 self.context.bind("$".to_string(), val);
11032 } else {
11033 self.context.unbind("$");
11034 }
11035
11036 sort_keys.push(sort_value);
11037 }
11038
11039 if let Some(tuple_bindings) = tuple_bindings {
11040 tuple_bindings.restore(self);
11041 }
11042
11043 indexed_array.push((idx, sort_keys));
11044 }
11045
11046 // Validate that all sort keys are comparable (same type, or undefined)
11047 // Undefined values (missing fields) are allowed and sort to the end
11048 // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
11049 for term_idx in 0..terms.len() {
11050 let mut first_valid_type: Option<&str> = None;
11051
11052 for (_idx, sort_keys) in &indexed_array {
11053 let sort_value = &sort_keys[term_idx];
11054
11055 // Skip undefined markers (missing fields) - these are allowed and sort to end
11056 if sort_value.is_undefined() {
11057 continue;
11058 }
11059
11060 // Get the type name for this value
11061 // Note: explicit null is NOT allowed - typeof null === 'object' in JS
11062 let value_type = match sort_value {
11063 JValue::Number(_) => "number",
11064 JValue::String(_) => "string",
11065 JValue::Bool(_) => "boolean",
11066 JValue::Array(_) => "array",
11067 JValue::Object(_) => "object", // This catches non-undefined objects
11068 JValue::Null => "null", // Explicit null from data
11069 _ => "unknown",
11070 };
11071
11072 // Check that sort keys are only numbers or strings
11073 // Null, boolean, array, and object types are not valid for sorting
11074 if value_type != "number" && value_type != "string" {
11075 return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
11076 }
11077
11078 // Check if this matches the first valid type we saw
11079 if let Some(first_type) = first_valid_type {
11080 if first_type != value_type {
11081 return Err(EvaluatorError::TypeError(format!(
11082 "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
11083 first_type, value_type
11084 )));
11085 }
11086 } else {
11087 first_valid_type = Some(value_type);
11088 }
11089 }
11090 }
11091
11092 // Sort the indexed array
11093 indexed_array.sort_by(|a, b| {
11094 // Compare sort keys in order
11095 for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
11096 let left = &a.1[i];
11097 let right = &b.1[i];
11098
11099 let cmp = self.compare_values(left, right);
11100
11101 if cmp != std::cmp::Ordering::Equal {
11102 return if *ascending { cmp } else { cmp.reverse() };
11103 }
11104 }
11105
11106 // If all keys are equal, maintain original order (stable sort)
11107 a.0.cmp(&b.0)
11108 });
11109
11110 // Extract sorted elements
11111 let sorted: Vec<JValue> = indexed_array
11112 .iter()
11113 .map(|(idx, _)| array[*idx].clone())
11114 .collect();
11115
11116 Ok(JValue::array(sorted))
11117 }
11118
11119 /// Compare two values for sorting (JSONata semantics)
11120 fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
11121 // Handle undefined markers first - they sort to the end
11122 let left_undef = left.is_undefined();
11123 let right_undef = right.is_undefined();
11124
11125 if left_undef && right_undef {
11126 return Ordering::Equal;
11127 }
11128 if left_undef {
11129 return Ordering::Greater; // Undefined sorts last
11130 }
11131 if right_undef {
11132 return Ordering::Less;
11133 }
11134
11135 match (left, right) {
11136 // Nulls also sort last (explicit null in data)
11137 (JValue::Null, JValue::Null) => Ordering::Equal,
11138 (JValue::Null, _) => Ordering::Greater,
11139 (_, JValue::Null) => Ordering::Less,
11140
11141 // Numbers
11142 (JValue::Number(a), JValue::Number(b)) => {
11143 let a_f64 = *a;
11144 let b_f64 = *b;
11145 a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
11146 }
11147
11148 // Strings
11149 (JValue::String(a), JValue::String(b)) => a.cmp(b),
11150
11151 // Booleans
11152 (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
11153
11154 // Arrays (lexicographic comparison)
11155 (JValue::Array(a), JValue::Array(b)) => {
11156 for (a_elem, b_elem) in a.iter().zip(b.iter()) {
11157 let cmp = self.compare_values(a_elem, b_elem);
11158 if cmp != Ordering::Equal {
11159 return cmp;
11160 }
11161 }
11162 a.len().cmp(&b.len())
11163 }
11164
11165 // Different types: use type ordering
11166 // null < bool < number < string < array < object
11167 (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
11168 (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
11169 (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
11170 (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
11171
11172 (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
11173 (JValue::Number(_), JValue::String(_)) => Ordering::Less,
11174 (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
11175 (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
11176
11177 (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
11178 (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
11179 (JValue::String(_), JValue::Array(_)) => Ordering::Less,
11180 (JValue::String(_), JValue::Object(_)) => Ordering::Less,
11181
11182 (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
11183 (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
11184 (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
11185 (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
11186
11187 (JValue::Object(_), _) => Ordering::Greater,
11188 _ => Ordering::Equal,
11189 }
11190 }
11191
11192 /// Check if a value is truthy (JSONata semantics).
11193 fn is_truthy(&self, value: &JValue) -> bool {
11194 match value {
11195 JValue::Null | JValue::Undefined => false,
11196 JValue::Bool(b) => *b,
11197 JValue::Number(n) => *n != 0.0,
11198 JValue::String(s) => !s.is_empty(),
11199 JValue::Array(arr) => !arr.is_empty(),
11200 JValue::Object(obj) => !obj.is_empty(),
11201 _ => false,
11202 }
11203 }
11204
11205 /// Check if a value is truthy for the default operator (?:)
11206 /// This has special semantics:
11207 /// - Lambda/function objects are not values, so they're falsy
11208 /// - Arrays containing only falsy elements are falsy
11209 /// - Otherwise, use standard truthiness
11210 fn is_truthy_for_default(&self, value: &JValue) -> bool {
11211 match value {
11212 // Lambda/function values are not data values, so they're falsy
11213 JValue::Lambda { .. } | JValue::Builtin { .. } => false,
11214 // Arrays need special handling - check if all elements are falsy
11215 JValue::Array(arr) => {
11216 if arr.is_empty() {
11217 return false;
11218 }
11219 // Array is truthy only if it contains at least one truthy element
11220 arr.iter().any(|elem| self.is_truthy(elem))
11221 }
11222 // For all other types, use standard truthiness
11223 _ => self.is_truthy(value),
11224 }
11225 }
11226
11227 /// Unwrap singleton arrays to scalar values
11228 /// This is used when no explicit array-keeping operation (like []) was used
11229 fn unwrap_singleton(&self, value: JValue) -> JValue {
11230 match value {
11231 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
11232 _ => value,
11233 }
11234 }
11235
11236 /// Extract lambda IDs from a value (used for closure preservation)
11237 /// Finds any lambda_id references in the value so they can be preserved
11238 /// when exiting a block scope
11239 fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
11240 // Fast path: scalars can never contain lambda references
11241 match value {
11242 JValue::Number(_)
11243 | JValue::Bool(_)
11244 | JValue::String(_)
11245 | JValue::Null
11246 | JValue::Undefined
11247 | JValue::Regex { .. }
11248 | JValue::Builtin { .. } => return Vec::new(),
11249 _ => {}
11250 }
11251 let mut ids = Vec::new();
11252 self.collect_lambda_ids(value, &mut ids);
11253 ids
11254 }
11255
11256 fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
11257 match value {
11258 JValue::Lambda { lambda_id, .. } => {
11259 let id_str = lambda_id.to_string();
11260 if !ids.contains(&id_str) {
11261 ids.push(id_str);
11262 // Transitively follow the stored lambda's captured_env
11263 // to find all referenced lambdas. This is critical for
11264 // closures like the Y-combinator where returned lambdas
11265 // capture other lambdas in their environment.
11266 if let Some(stored) = self.context.lookup_lambda(lambda_id) {
11267 let env_values: Vec<JValue> =
11268 stored.captured_env.values().cloned().collect();
11269 for env_value in &env_values {
11270 self.collect_lambda_ids(env_value, ids);
11271 }
11272 }
11273 }
11274 }
11275 JValue::Object(map) => {
11276 // Recurse into object values
11277 for v in map.values() {
11278 self.collect_lambda_ids(v, ids);
11279 }
11280 }
11281 JValue::Array(arr) => {
11282 // Recurse into array elements
11283 for v in arr.iter() {
11284 self.collect_lambda_ids(v, ids);
11285 }
11286 }
11287 _ => {}
11288 }
11289 }
11290
11291 /// Equality comparison (JSONata semantics)
11292 fn equals(&self, left: &JValue, right: &JValue) -> bool {
11293 crate::functions::array::values_equal(left, right)
11294 }
11295
11296 /// Addition
11297 fn add(
11298 &self,
11299 left: &JValue,
11300 right: &JValue,
11301 left_is_explicit_null: bool,
11302 right_is_explicit_null: bool,
11303 ) -> Result<JValue, EvaluatorError> {
11304 match (left, right) {
11305 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
11306 // Explicit null literal with number -> T2002 error
11307 (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
11308 Err(EvaluatorError::TypeError(
11309 "T2002: The left side of the + operator must evaluate to a number".to_string(),
11310 ))
11311 }
11312 (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
11313 Err(EvaluatorError::TypeError(
11314 "T2002: The right side of the + operator must evaluate to a number".to_string(),
11315 ))
11316 }
11317 (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
11318 Err(EvaluatorError::TypeError(
11319 "T2002: The left side of the + operator must evaluate to a number".to_string(),
11320 ))
11321 }
11322 // Undefined variable (null/undefined) with number -> undefined result
11323 (JValue::Null | JValue::Undefined, JValue::Number(_))
11324 | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
11325 // Boolean with anything (including undefined) -> T2001 error
11326 (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
11327 "T2001: The left side of the '+' operator must evaluate to a number or a string"
11328 .to_string(),
11329 )),
11330 (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
11331 "T2001: The right side of the '+' operator must evaluate to a number or a string"
11332 .to_string(),
11333 )),
11334 // Undefined with undefined -> undefined
11335 (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
11336 Ok(JValue::Null)
11337 }
11338 _ => Err(EvaluatorError::TypeError(format!(
11339 "Cannot add {:?} and {:?}",
11340 left, right
11341 ))),
11342 }
11343 }
11344
11345 /// Subtraction
11346 fn subtract(
11347 &self,
11348 left: &JValue,
11349 right: &JValue,
11350 left_is_explicit_null: bool,
11351 right_is_explicit_null: bool,
11352 ) -> Result<JValue, EvaluatorError> {
11353 match (left, right) {
11354 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
11355 // Explicit null literal -> error
11356 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11357 "T2002: The left side of the - operator must evaluate to a number".to_string(),
11358 )),
11359 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11360 "T2002: The right side of the - operator must evaluate to a number".to_string(),
11361 )),
11362 // Undefined variables -> undefined result
11363 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11364 Ok(JValue::Null)
11365 }
11366 _ => Err(EvaluatorError::TypeError(format!(
11367 "Cannot subtract {:?} and {:?}",
11368 left, right
11369 ))),
11370 }
11371 }
11372
11373 /// Multiplication
11374 fn multiply(
11375 &self,
11376 left: &JValue,
11377 right: &JValue,
11378 left_is_explicit_null: bool,
11379 right_is_explicit_null: bool,
11380 ) -> Result<JValue, EvaluatorError> {
11381 match (left, right) {
11382 (JValue::Number(a), JValue::Number(b)) => {
11383 let result = *a * *b;
11384 // Check for overflow to Infinity
11385 if result.is_infinite() {
11386 return Err(EvaluatorError::EvaluationError(
11387 "D1001: Number out of range".to_string(),
11388 ));
11389 }
11390 Ok(JValue::Number(result))
11391 }
11392 // Explicit null literal -> error
11393 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11394 "T2002: The left side of the * operator must evaluate to a number".to_string(),
11395 )),
11396 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11397 "T2002: The right side of the * operator must evaluate to a number".to_string(),
11398 )),
11399 // Undefined variables -> undefined result
11400 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11401 Ok(JValue::Null)
11402 }
11403 _ => Err(EvaluatorError::TypeError(format!(
11404 "Cannot multiply {:?} and {:?}",
11405 left, right
11406 ))),
11407 }
11408 }
11409
11410 /// Division
11411 fn divide(
11412 &self,
11413 left: &JValue,
11414 right: &JValue,
11415 left_is_explicit_null: bool,
11416 right_is_explicit_null: bool,
11417 ) -> Result<JValue, EvaluatorError> {
11418 match (left, right) {
11419 (JValue::Number(a), JValue::Number(b)) => {
11420 let denominator = *b;
11421 if denominator == 0.0 {
11422 return Err(EvaluatorError::EvaluationError(
11423 "Division by zero".to_string(),
11424 ));
11425 }
11426 Ok(JValue::Number(*a / denominator))
11427 }
11428 // Explicit null literal -> error
11429 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11430 "T2002: The left side of the / operator must evaluate to a number".to_string(),
11431 )),
11432 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11433 "T2002: The right side of the / operator must evaluate to a number".to_string(),
11434 )),
11435 // Undefined variables -> undefined result
11436 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11437 Ok(JValue::Null)
11438 }
11439 _ => Err(EvaluatorError::TypeError(format!(
11440 "Cannot divide {:?} and {:?}",
11441 left, right
11442 ))),
11443 }
11444 }
11445
11446 /// Modulo
11447 fn modulo(
11448 &self,
11449 left: &JValue,
11450 right: &JValue,
11451 left_is_explicit_null: bool,
11452 right_is_explicit_null: bool,
11453 ) -> Result<JValue, EvaluatorError> {
11454 match (left, right) {
11455 (JValue::Number(a), JValue::Number(b)) => {
11456 let denominator = *b;
11457 if denominator == 0.0 {
11458 return Err(EvaluatorError::EvaluationError(
11459 "Division by zero".to_string(),
11460 ));
11461 }
11462 Ok(JValue::Number(*a % denominator))
11463 }
11464 // Explicit null literal -> error
11465 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11466 "T2002: The left side of the % operator must evaluate to a number".to_string(),
11467 )),
11468 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11469 "T2002: The right side of the % operator must evaluate to a number".to_string(),
11470 )),
11471 // Undefined variables -> undefined result
11472 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11473 Ok(JValue::Null)
11474 }
11475 _ => Err(EvaluatorError::TypeError(format!(
11476 "Cannot compute modulo of {:?} and {:?}",
11477 left, right
11478 ))),
11479 }
11480 }
11481
11482 /// Get human-readable type name for error messages
11483 fn type_name(value: &JValue) -> &'static str {
11484 match value {
11485 JValue::Null => "null",
11486 JValue::Bool(_) => "boolean",
11487 JValue::Number(_) => "number",
11488 JValue::String(_) => "string",
11489 JValue::Array(_) => "array",
11490 JValue::Object(_) => "object",
11491 _ => "unknown",
11492 }
11493 }
11494
11495 /// Ordered comparison with null/type checking shared across <, <=, >, >=
11496 ///
11497 /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
11498 /// `compare_strs` receives (left_str, right_str) for string operands.
11499 /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
11500 fn ordered_compare(
11501 &self,
11502 left: &JValue,
11503 right: &JValue,
11504 left_is_explicit_null: bool,
11505 right_is_explicit_null: bool,
11506 op_symbol: &str,
11507 compare_nums: fn(f64, f64) -> bool,
11508 compare_strs: fn(&str, &str) -> bool,
11509 ) -> Result<JValue, EvaluatorError> {
11510 match (left, right) {
11511 (JValue::Number(a), JValue::Number(b)) => {
11512 Ok(JValue::Bool(compare_nums(*a, *b)))
11513 }
11514 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
11515 // Both null/undefined -> return undefined
11516 (JValue::Null, JValue::Null) => Ok(JValue::Null),
11517 // Explicit null literal with any type (except null) -> T2010 error
11518 (JValue::Null, _) if left_is_explicit_null => {
11519 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11520 }
11521 (_, JValue::Null) if right_is_explicit_null => {
11522 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11523 }
11524 // Boolean with undefined -> T2010 error
11525 (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
11526 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11527 }
11528 // Number or String with undefined (not explicit null) -> undefined result
11529 (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
11530 (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
11531 Ok(JValue::Null)
11532 }
11533 // String vs Number -> T2009
11534 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
11535 Err(EvaluatorError::EvaluationError(format!(
11536 "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
11537 op_symbol
11538 )))
11539 }
11540 // Boolean comparisons -> T2010
11541 (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
11542 Err(EvaluatorError::EvaluationError(format!(
11543 "T2010: Cannot compare {} and {}",
11544 Self::type_name(left), Self::type_name(right)
11545 )))
11546 }
11547 // Other type mismatches
11548 _ => Err(EvaluatorError::EvaluationError(format!(
11549 "T2010: Cannot compare {} and {}",
11550 Self::type_name(left), Self::type_name(right)
11551 ))),
11552 }
11553 }
11554
11555 /// Less than comparison
11556 fn less_than(
11557 &self,
11558 left: &JValue,
11559 right: &JValue,
11560 left_is_explicit_null: bool,
11561 right_is_explicit_null: bool,
11562 ) -> Result<JValue, EvaluatorError> {
11563 self.ordered_compare(
11564 left,
11565 right,
11566 left_is_explicit_null,
11567 right_is_explicit_null,
11568 "<",
11569 |a, b| a < b,
11570 |a, b| a < b,
11571 )
11572 }
11573
11574 /// Less than or equal comparison
11575 fn less_than_or_equal(
11576 &self,
11577 left: &JValue,
11578 right: &JValue,
11579 left_is_explicit_null: bool,
11580 right_is_explicit_null: bool,
11581 ) -> Result<JValue, EvaluatorError> {
11582 self.ordered_compare(
11583 left,
11584 right,
11585 left_is_explicit_null,
11586 right_is_explicit_null,
11587 "<=",
11588 |a, b| a <= b,
11589 |a, b| a <= b,
11590 )
11591 }
11592
11593 /// Greater than comparison
11594 fn greater_than(
11595 &self,
11596 left: &JValue,
11597 right: &JValue,
11598 left_is_explicit_null: bool,
11599 right_is_explicit_null: bool,
11600 ) -> Result<JValue, EvaluatorError> {
11601 self.ordered_compare(
11602 left,
11603 right,
11604 left_is_explicit_null,
11605 right_is_explicit_null,
11606 ">",
11607 |a, b| a > b,
11608 |a, b| a > b,
11609 )
11610 }
11611
11612 /// Greater than or equal comparison
11613 fn greater_than_or_equal(
11614 &self,
11615 left: &JValue,
11616 right: &JValue,
11617 left_is_explicit_null: bool,
11618 right_is_explicit_null: bool,
11619 ) -> Result<JValue, EvaluatorError> {
11620 self.ordered_compare(
11621 left,
11622 right,
11623 left_is_explicit_null,
11624 right_is_explicit_null,
11625 ">=",
11626 |a, b| a >= b,
11627 |a, b| a >= b,
11628 )
11629 }
11630
11631 /// Convert a value to a string for concatenation
11632 fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
11633 match value {
11634 JValue::String(s) => Ok(s.to_string()),
11635 JValue::Null => Ok(String::new()),
11636 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
11637 match crate::functions::string::string(value, None) {
11638 Ok(JValue::String(s)) => Ok(s.to_string()),
11639 Ok(JValue::Null) => Ok(String::new()),
11640 _ => Err(EvaluatorError::TypeError(
11641 "Cannot concatenate complex types".to_string(),
11642 )),
11643 }
11644 }
11645 _ => Ok(String::new()),
11646 }
11647 }
11648
11649 /// String concatenation
11650 fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11651 let left_str = Self::value_to_concat_string(left)?;
11652 let right_str = Self::value_to_concat_string(right)?;
11653 Ok(JValue::string(format!("{}{}", left_str, right_str)))
11654 }
11655
11656 /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
11657 fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11658 // Check left operand is a number or null
11659 let start_f64 = match left {
11660 JValue::Number(n) => Some(*n),
11661 JValue::Null | JValue::Undefined => None,
11662 _ => {
11663 return Err(EvaluatorError::EvaluationError(
11664 "T2003: Left operand of range operator must be a number".to_string(),
11665 ));
11666 }
11667 };
11668
11669 // Check left operand is an integer (if it's a number)
11670 if let Some(val) = start_f64 {
11671 if val.fract() != 0.0 {
11672 return Err(EvaluatorError::EvaluationError(
11673 "T2003: Left operand of range operator must be an integer".to_string(),
11674 ));
11675 }
11676 }
11677
11678 // Check right operand is a number or null
11679 let end_f64 = match right {
11680 JValue::Number(n) => Some(*n),
11681 JValue::Null | JValue::Undefined => None,
11682 _ => {
11683 return Err(EvaluatorError::EvaluationError(
11684 "T2004: Right operand of range operator must be a number".to_string(),
11685 ));
11686 }
11687 };
11688
11689 // Check right operand is an integer (if it's a number)
11690 if let Some(val) = end_f64 {
11691 if val.fract() != 0.0 {
11692 return Err(EvaluatorError::EvaluationError(
11693 "T2004: Right operand of range operator must be an integer".to_string(),
11694 ));
11695 }
11696 }
11697
11698 // If either operand is null, return empty array
11699 if start_f64.is_none() || end_f64.is_none() {
11700 return Ok(JValue::array(vec![]));
11701 }
11702
11703 let start = start_f64.unwrap() as i64;
11704 let end = end_f64.unwrap() as i64;
11705
11706 // Check range size limit (10 million elements max)
11707 let size = if start <= end {
11708 (end - start + 1) as usize
11709 } else {
11710 0
11711 };
11712 if size > 10_000_000 {
11713 return Err(EvaluatorError::EvaluationError(
11714 "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
11715 ));
11716 }
11717 check_sequence_length(size, &self.options)?;
11718
11719 let mut result = Vec::with_capacity(size);
11720 if start <= end {
11721 for i in start..=end {
11722 result.push(JValue::Number(i as f64));
11723 }
11724 }
11725 // Note: if start > end, return empty array (not reversed)
11726 Ok(JValue::array(result))
11727 }
11728
11729 /// In operator (checks if left is in right array/object)
11730 /// Array indexing: array[index]
11731 fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
11732 match (array, index) {
11733 (JValue::Array(arr), JValue::Number(n)) => {
11734 let idx = *n as i64;
11735 let len = arr.len() as i64;
11736
11737 // Handle negative indexing (offset from end)
11738 let actual_idx = if idx < 0 { len + idx } else { idx };
11739
11740 if actual_idx < 0 || actual_idx >= len {
11741 Ok(JValue::Undefined)
11742 } else {
11743 Ok(arr[actual_idx as usize].clone())
11744 }
11745 }
11746 _ => Err(EvaluatorError::TypeError(
11747 "Array indexing requires array and number".to_string(),
11748 )),
11749 }
11750 }
11751
11752 /// Array filtering: array[predicate]
11753 /// Evaluates the predicate for each item in the array and returns items where predicate is true
11754 fn array_filter(
11755 &mut self,
11756 _lhs_node: &AstNode,
11757 rhs_node: &AstNode,
11758 array: &JValue,
11759 _original_data: &JValue,
11760 ) -> Result<JValue, EvaluatorError> {
11761 match array {
11762 JValue::Array(arr) => {
11763 // Pre-allocate with estimated capacity (assume ~50% will match)
11764 let mut filtered = Vec::with_capacity(arr.len() / 2);
11765
11766 for item in arr.iter() {
11767 // Evaluate the predicate in the context of this array item
11768 // The item becomes the new "current context" ($)
11769 let predicate_result = self.evaluate_internal(rhs_node, item)?;
11770
11771 // Check if the predicate is truthy
11772 if self.is_truthy(&predicate_result) {
11773 filtered.push(item.clone());
11774 }
11775 }
11776
11777 Ok(JValue::array(filtered))
11778 }
11779 _ => Err(EvaluatorError::TypeError(
11780 "Array filtering requires an array".to_string(),
11781 )),
11782 }
11783 }
11784
11785 fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11786 // If either side is undefined/null, return false (not an error)
11787 // This matches JavaScript behavior
11788 if left.is_null() || right.is_null() {
11789 return Ok(JValue::Bool(false));
11790 }
11791
11792 match right {
11793 JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
11794 JValue::Object(obj) => {
11795 if let JValue::String(key) = left {
11796 Ok(JValue::Bool(obj.contains_key(&**key)))
11797 } else {
11798 Ok(JValue::Bool(false))
11799 }
11800 }
11801 // If right side is not an array or object (e.g., string, number),
11802 // wrap it in an array for comparison
11803 other => Ok(JValue::Bool(self.equals(left, other))),
11804 }
11805 }
11806
11807 /// Create a partially applied function from a function call with placeholder arguments
11808 /// This evaluates non-placeholder arguments and creates a new lambda that takes
11809 /// the placeholder positions as parameters.
11810 fn create_partial_application(
11811 &mut self,
11812 name: &str,
11813 args: &[AstNode],
11814 is_builtin: bool,
11815 data: &JValue,
11816 ) -> Result<JValue, EvaluatorError> {
11817 // First, look up the function to ensure it exists
11818 let is_lambda = self.context.lookup_lambda(name).is_some()
11819 || (self
11820 .context
11821 .lookup(name)
11822 .map(|v| matches!(v, JValue::Lambda { .. }))
11823 .unwrap_or(false));
11824
11825 // Built-in functions must be called with $ prefix for partial application
11826 // Without $, it's an error (T1007) suggesting the user forgot the $
11827 if !is_lambda && !is_builtin {
11828 // Check if it's a built-in function called without $
11829 if self.is_builtin_function(name) {
11830 return Err(EvaluatorError::EvaluationError(format!(
11831 "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
11832 name
11833 )));
11834 }
11835 return Err(EvaluatorError::EvaluationError(
11836 "T1008: Attempted to partially apply a non-function".to_string(),
11837 ));
11838 }
11839
11840 // Evaluate non-placeholder arguments and track placeholder positions
11841 let mut bound_args: Vec<(usize, JValue)> = Vec::new();
11842 let mut placeholder_positions: Vec<usize> = Vec::new();
11843
11844 for (i, arg) in args.iter().enumerate() {
11845 if matches!(arg, AstNode::Placeholder) {
11846 placeholder_positions.push(i);
11847 } else {
11848 let value = self.evaluate_internal(arg, data)?;
11849 bound_args.push((i, value));
11850 }
11851 }
11852
11853 // Generate parameter names for each placeholder
11854 let param_names: Vec<String> = placeholder_positions
11855 .iter()
11856 .enumerate()
11857 .map(|(i, _)| format!("__p{}", i))
11858 .collect();
11859
11860 // Store the partial application info as a special lambda
11861 // When invoked, it will call the original function with bound + placeholder args
11862 let partial_id = format!(
11863 "__partial_{}_{}_{}",
11864 name,
11865 placeholder_positions.len(),
11866 bound_args.len()
11867 );
11868
11869 // Create a stored lambda that represents this partial application
11870 // The body is a marker that we'll interpret specially during invocation
11871 let stored_lambda = StoredLambda {
11872 params: param_names.clone(),
11873 body: AstNode::String(format!(
11874 "__partial_call:{}:{}:{}",
11875 name,
11876 is_builtin,
11877 args.len()
11878 )),
11879 compiled_body: None, // Partial application uses a special body marker
11880 signature: None,
11881 captured_env: {
11882 let mut env = self.capture_current_environment();
11883 // Store the bound arguments in the captured environment
11884 for (pos, value) in &bound_args {
11885 env.insert(format!("__bound_arg_{}", pos), value.clone());
11886 }
11887 // Store placeholder positions
11888 env.insert(
11889 "__placeholder_positions".to_string(),
11890 JValue::array(
11891 placeholder_positions
11892 .iter()
11893 .map(|p| JValue::Number(*p as f64))
11894 .collect::<Vec<_>>(),
11895 ),
11896 );
11897 // Store total argument count
11898 env.insert(
11899 "__total_args".to_string(),
11900 JValue::Number(args.len() as f64),
11901 );
11902 env
11903 },
11904 captured_data: Some(data.clone()),
11905 thunk: false,
11906 };
11907
11908 self.context.bind_lambda(partial_id.clone(), stored_lambda);
11909
11910 // Return a lambda object that can be invoked
11911 let lambda_obj = JValue::lambda(
11912 partial_id.as_str(),
11913 param_names,
11914 Some(name.to_string()),
11915 None::<String>,
11916 );
11917
11918 Ok(lambda_obj)
11919 }
11920}
11921
11922impl Default for Evaluator {
11923 fn default() -> Self {
11924 Self::new()
11925 }
11926}
11927
11928#[cfg(test)]
11929mod tests {
11930 use super::*;
11931 use crate::ast::{BinaryOp, UnaryOp};
11932
11933 // --- Task 7: tuple-wrapper output leak -----------------------------------
11934 //
11935 // `%`/`@`/`#` are implemented internally via a tuple-stream representation
11936 // (`create_tuple_stream`): each element gets wrapped as
11937 // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
11938 // consume/re-wrap these, but the *final* evaluate() result can still carry
11939 // a lingering wrapper -- confirmed for real by dumping actual output before
11940 // this fix (see task-7-report.md for the raw before/after). These tests
11941 // pin both the bare top-level case (Task 5's brief `#` example) and the
11942 // object/array-construction-nested case (found while verifying the brief's
11943 // illustrative fix against real output -- a plain per-element Array-only
11944 // recursion does not reach into a constructed object's field values).
11945
11946 fn dataset5_for_tuple_tests() -> JValue {
11947 let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
11948 serde_json::from_str::<serde_json::Value>(s).unwrap().into()
11949 }
11950
11951 fn assert_no_tuple_wrapper(value: &JValue) {
11952 match value {
11953 JValue::Object(obj) => {
11954 assert!(
11955 obj.get("__tuple__").is_none(),
11956 "tuple wrapper leaked into output: {:?}",
11957 value
11958 );
11959 for v in obj.values() {
11960 assert_no_tuple_wrapper(v);
11961 }
11962 }
11963 JValue::Array(arr) => {
11964 for item in arr.iter() {
11965 assert_no_tuple_wrapper(item);
11966 }
11967 }
11968 _ => {}
11969 }
11970 }
11971
11972 #[test]
11973 fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
11974 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11975 let ast = crate::parser::parse("items#$i").unwrap();
11976 let mut evaluator = Evaluator::new();
11977 let result = evaluator.evaluate(&ast, &data).unwrap();
11978 assert_no_tuple_wrapper(&result);
11979 assert_eq!(
11980 result,
11981 JValue::array(vec![
11982 JValue::from(1i64),
11983 JValue::from(2i64),
11984 JValue::from(3i64)
11985 ])
11986 );
11987 }
11988
11989 #[test]
11990 fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
11991 // Confirmed by Task 6 to evaluate to the correct @-values but stay
11992 // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
11993 let data = dataset5_for_tuple_tests();
11994 let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
11995 let mut evaluator = Evaluator::new();
11996 let result = evaluator.evaluate(&ast, &data).unwrap();
11997 assert_no_tuple_wrapper(&result);
11998 assert_eq!(
11999 result,
12000 JValue::array(vec![
12001 JValue::string("040657863"),
12002 JValue::string("0406654603"),
12003 ])
12004 );
12005 }
12006
12007 #[test]
12008 fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
12009 // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
12010 let data = dataset5_for_tuple_tests();
12011 let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
12012 .unwrap();
12013 let mut evaluator = Evaluator::new();
12014 let result = evaluator.evaluate(&ast, &data).unwrap();
12015 assert_no_tuple_wrapper(&result);
12016 assert_eq!(
12017 result,
12018 JValue::array(vec![
12019 JValue::string("0406654608"),
12020 JValue::string("0406634348"),
12021 ])
12022 );
12023 }
12024
12025 #[test]
12026 fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
12027 // A tuple-producing expression nested inside a constructed object's field
12028 // value: the top-level result is a plain (non-tuple) Object, so a naive
12029 // "unwrap only if the whole value is a tuple wrapper" check would miss
12030 // this -- must recurse into field values too.
12031 let data = dataset5_for_tuple_tests();
12032 let ast =
12033 crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
12034 .unwrap();
12035 let mut evaluator = Evaluator::new();
12036 let result = evaluator.evaluate(&ast, &data).unwrap();
12037 assert_no_tuple_wrapper(&result);
12038 assert_eq!(
12039 result,
12040 JValue::from(serde_json::json!({
12041 "skus": ["040657863", "0406654603"]
12042 }))
12043 );
12044 }
12045
12046 #[test]
12047 fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
12048 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12049 let ast = crate::parser::parse("[items#$i]").unwrap();
12050 let mut evaluator = Evaluator::new();
12051 let result = evaluator.evaluate(&ast, &data).unwrap();
12052 assert_no_tuple_wrapper(&result);
12053 }
12054
12055 #[test]
12056 fn test_evaluate_literals() {
12057 let mut evaluator = Evaluator::new();
12058 let data = JValue::Null;
12059
12060 // String literal
12061 let result = evaluator
12062 .evaluate(&AstNode::string("hello"), &data)
12063 .unwrap();
12064 assert_eq!(result, JValue::string("hello"));
12065
12066 // Number literal
12067 let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
12068 assert_eq!(result, JValue::from(42i64));
12069
12070 // Boolean literal
12071 let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
12072 assert_eq!(result, JValue::Bool(true));
12073
12074 // Null literal
12075 let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
12076 assert_eq!(result, JValue::Null);
12077 }
12078
12079 #[test]
12080 fn test_evaluate_variables() {
12081 let mut evaluator = Evaluator::new();
12082 let data = JValue::Null;
12083
12084 // Bind a variable
12085 evaluator
12086 .context
12087 .bind("x".to_string(), JValue::from(100i64));
12088
12089 // Look up the variable
12090 let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
12091 assert_eq!(result, JValue::from(100i64));
12092
12093 // Undefined variable returns null (undefined in JSONata semantics)
12094 let result = evaluator
12095 .evaluate(&AstNode::variable("undefined"), &data)
12096 .unwrap();
12097 assert_eq!(result, JValue::Null);
12098 }
12099
12100 #[test]
12101 fn test_evaluate_path() {
12102 let mut evaluator = Evaluator::new();
12103 let data = JValue::from(serde_json::json!({
12104 "foo": {
12105 "bar": {
12106 "baz": 42
12107 }
12108 }
12109 }));
12110 // Simple path
12111 let path = AstNode::Path {
12112 steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
12113 };
12114 let result = evaluator.evaluate(&path, &data).unwrap();
12115 assert_eq!(
12116 result,
12117 JValue::from(serde_json::json!({"bar": {"baz": 42}}))
12118 );
12119
12120 // Nested path
12121 let path = AstNode::Path {
12122 steps: vec![
12123 PathStep::new(AstNode::Name("foo".to_string())),
12124 PathStep::new(AstNode::Name("bar".to_string())),
12125 PathStep::new(AstNode::Name("baz".to_string())),
12126 ],
12127 };
12128 let result = evaluator.evaluate(&path, &data).unwrap();
12129 assert_eq!(result, JValue::from(42i64));
12130
12131 // Missing path returns undefined (not null - see issue #32)
12132 let path = AstNode::Path {
12133 steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
12134 };
12135 let result = evaluator.evaluate(&path, &data).unwrap();
12136 assert_eq!(result, JValue::Undefined);
12137 }
12138
12139 #[test]
12140 fn test_arithmetic_operations() {
12141 let mut evaluator = Evaluator::new();
12142 let data = JValue::Null;
12143
12144 // Addition
12145 let expr = AstNode::Binary {
12146 op: BinaryOp::Add,
12147 lhs: Box::new(AstNode::number(10.0)),
12148 rhs: Box::new(AstNode::number(5.0)),
12149 };
12150 let result = evaluator.evaluate(&expr, &data).unwrap();
12151 assert_eq!(result, JValue::Number(15.0));
12152
12153 // Subtraction
12154 let expr = AstNode::Binary {
12155 op: BinaryOp::Subtract,
12156 lhs: Box::new(AstNode::number(10.0)),
12157 rhs: Box::new(AstNode::number(5.0)),
12158 };
12159 let result = evaluator.evaluate(&expr, &data).unwrap();
12160 assert_eq!(result, JValue::Number(5.0));
12161
12162 // Multiplication
12163 let expr = AstNode::Binary {
12164 op: BinaryOp::Multiply,
12165 lhs: Box::new(AstNode::number(10.0)),
12166 rhs: Box::new(AstNode::number(5.0)),
12167 };
12168 let result = evaluator.evaluate(&expr, &data).unwrap();
12169 assert_eq!(result, JValue::Number(50.0));
12170
12171 // Division
12172 let expr = AstNode::Binary {
12173 op: BinaryOp::Divide,
12174 lhs: Box::new(AstNode::number(10.0)),
12175 rhs: Box::new(AstNode::number(5.0)),
12176 };
12177 let result = evaluator.evaluate(&expr, &data).unwrap();
12178 assert_eq!(result, JValue::Number(2.0));
12179
12180 // Modulo
12181 let expr = AstNode::Binary {
12182 op: BinaryOp::Modulo,
12183 lhs: Box::new(AstNode::number(10.0)),
12184 rhs: Box::new(AstNode::number(3.0)),
12185 };
12186 let result = evaluator.evaluate(&expr, &data).unwrap();
12187 assert_eq!(result, JValue::Number(1.0));
12188 }
12189
12190 #[test]
12191 fn test_division_by_zero() {
12192 let mut evaluator = Evaluator::new();
12193 let data = JValue::Null;
12194
12195 let expr = AstNode::Binary {
12196 op: BinaryOp::Divide,
12197 lhs: Box::new(AstNode::number(10.0)),
12198 rhs: Box::new(AstNode::number(0.0)),
12199 };
12200 let result = evaluator.evaluate(&expr, &data);
12201 assert!(result.is_err());
12202 }
12203
12204 #[test]
12205 fn test_comparison_operations() {
12206 let mut evaluator = Evaluator::new();
12207 let data = JValue::Null;
12208
12209 // Equal
12210 let expr = AstNode::Binary {
12211 op: BinaryOp::Equal,
12212 lhs: Box::new(AstNode::number(5.0)),
12213 rhs: Box::new(AstNode::number(5.0)),
12214 };
12215 assert_eq!(
12216 evaluator.evaluate(&expr, &data).unwrap(),
12217 JValue::Bool(true)
12218 );
12219
12220 // Not equal
12221 let expr = AstNode::Binary {
12222 op: BinaryOp::NotEqual,
12223 lhs: Box::new(AstNode::number(5.0)),
12224 rhs: Box::new(AstNode::number(3.0)),
12225 };
12226 assert_eq!(
12227 evaluator.evaluate(&expr, &data).unwrap(),
12228 JValue::Bool(true)
12229 );
12230
12231 // Less than
12232 let expr = AstNode::Binary {
12233 op: BinaryOp::LessThan,
12234 lhs: Box::new(AstNode::number(3.0)),
12235 rhs: Box::new(AstNode::number(5.0)),
12236 };
12237 assert_eq!(
12238 evaluator.evaluate(&expr, &data).unwrap(),
12239 JValue::Bool(true)
12240 );
12241
12242 // Greater than
12243 let expr = AstNode::Binary {
12244 op: BinaryOp::GreaterThan,
12245 lhs: Box::new(AstNode::number(5.0)),
12246 rhs: Box::new(AstNode::number(3.0)),
12247 };
12248 assert_eq!(
12249 evaluator.evaluate(&expr, &data).unwrap(),
12250 JValue::Bool(true)
12251 );
12252 }
12253
12254 #[test]
12255 fn test_logical_operations() {
12256 let mut evaluator = Evaluator::new();
12257 let data = JValue::Null;
12258
12259 // And - both true
12260 let expr = AstNode::Binary {
12261 op: BinaryOp::And,
12262 lhs: Box::new(AstNode::boolean(true)),
12263 rhs: Box::new(AstNode::boolean(true)),
12264 };
12265 assert_eq!(
12266 evaluator.evaluate(&expr, &data).unwrap(),
12267 JValue::Bool(true)
12268 );
12269
12270 // And - first false
12271 let expr = AstNode::Binary {
12272 op: BinaryOp::And,
12273 lhs: Box::new(AstNode::boolean(false)),
12274 rhs: Box::new(AstNode::boolean(true)),
12275 };
12276 assert_eq!(
12277 evaluator.evaluate(&expr, &data).unwrap(),
12278 JValue::Bool(false)
12279 );
12280
12281 // Or - first true
12282 let expr = AstNode::Binary {
12283 op: BinaryOp::Or,
12284 lhs: Box::new(AstNode::boolean(true)),
12285 rhs: Box::new(AstNode::boolean(false)),
12286 };
12287 assert_eq!(
12288 evaluator.evaluate(&expr, &data).unwrap(),
12289 JValue::Bool(true)
12290 );
12291
12292 // Or - both false
12293 let expr = AstNode::Binary {
12294 op: BinaryOp::Or,
12295 lhs: Box::new(AstNode::boolean(false)),
12296 rhs: Box::new(AstNode::boolean(false)),
12297 };
12298 assert_eq!(
12299 evaluator.evaluate(&expr, &data).unwrap(),
12300 JValue::Bool(false)
12301 );
12302 }
12303
12304 #[test]
12305 fn test_string_concatenation() {
12306 let mut evaluator = Evaluator::new();
12307 let data = JValue::Null;
12308
12309 let expr = AstNode::Binary {
12310 op: BinaryOp::Concatenate,
12311 lhs: Box::new(AstNode::string("Hello")),
12312 rhs: Box::new(AstNode::string(" World")),
12313 };
12314 let result = evaluator.evaluate(&expr, &data).unwrap();
12315 assert_eq!(result, JValue::string("Hello World"));
12316 }
12317
12318 #[test]
12319 fn test_range_operator() {
12320 let mut evaluator = Evaluator::new();
12321 let data = JValue::Null;
12322
12323 // Forward range
12324 let expr = AstNode::Binary {
12325 op: BinaryOp::Range,
12326 lhs: Box::new(AstNode::number(1.0)),
12327 rhs: Box::new(AstNode::number(5.0)),
12328 };
12329 let result = evaluator.evaluate(&expr, &data).unwrap();
12330 assert_eq!(
12331 result,
12332 JValue::array(vec![
12333 JValue::Number(1.0),
12334 JValue::Number(2.0),
12335 JValue::Number(3.0),
12336 JValue::Number(4.0),
12337 JValue::Number(5.0)
12338 ])
12339 );
12340
12341 // Backward range (start > end) returns empty array
12342 let expr = AstNode::Binary {
12343 op: BinaryOp::Range,
12344 lhs: Box::new(AstNode::number(5.0)),
12345 rhs: Box::new(AstNode::number(1.0)),
12346 };
12347 let result = evaluator.evaluate(&expr, &data).unwrap();
12348 assert_eq!(result, JValue::array(vec![]));
12349 }
12350
12351 #[test]
12352 fn test_in_operator() {
12353 let mut evaluator = Evaluator::new();
12354 let data = JValue::Null;
12355
12356 // In array
12357 let expr = AstNode::Binary {
12358 op: BinaryOp::In,
12359 lhs: Box::new(AstNode::number(3.0)),
12360 rhs: Box::new(AstNode::Array(vec![
12361 AstNode::number(1.0),
12362 AstNode::number(2.0),
12363 AstNode::number(3.0),
12364 ])),
12365 };
12366 let result = evaluator.evaluate(&expr, &data).unwrap();
12367 assert_eq!(result, JValue::Bool(true));
12368
12369 // Not in array
12370 let expr = AstNode::Binary {
12371 op: BinaryOp::In,
12372 lhs: Box::new(AstNode::number(5.0)),
12373 rhs: Box::new(AstNode::Array(vec![
12374 AstNode::number(1.0),
12375 AstNode::number(2.0),
12376 AstNode::number(3.0),
12377 ])),
12378 };
12379 let result = evaluator.evaluate(&expr, &data).unwrap();
12380 assert_eq!(result, JValue::Bool(false));
12381 }
12382
12383 #[test]
12384 fn test_unary_operations() {
12385 let mut evaluator = Evaluator::new();
12386 let data = JValue::Null;
12387
12388 // Negation
12389 let expr = AstNode::Unary {
12390 op: UnaryOp::Negate,
12391 operand: Box::new(AstNode::number(5.0)),
12392 };
12393 let result = evaluator.evaluate(&expr, &data).unwrap();
12394 assert_eq!(result, JValue::Number(-5.0));
12395
12396 // Not
12397 let expr = AstNode::Unary {
12398 op: UnaryOp::Not,
12399 operand: Box::new(AstNode::boolean(true)),
12400 };
12401 let result = evaluator.evaluate(&expr, &data).unwrap();
12402 assert_eq!(result, JValue::Bool(false));
12403 }
12404
12405 #[test]
12406 fn test_array_construction() {
12407 let mut evaluator = Evaluator::new();
12408 let data = JValue::Null;
12409
12410 let expr = AstNode::Array(vec![
12411 AstNode::number(1.0),
12412 AstNode::number(2.0),
12413 AstNode::number(3.0),
12414 ]);
12415 let result = evaluator.evaluate(&expr, &data).unwrap();
12416 // Whole number literals are preserved as integers
12417 assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
12418 }
12419
12420 #[test]
12421 fn test_object_construction() {
12422 let mut evaluator = Evaluator::new();
12423 let data = JValue::Null;
12424
12425 let expr = AstNode::Object(vec![
12426 (AstNode::string("name"), AstNode::string("Alice")),
12427 (AstNode::string("age"), AstNode::number(30.0)),
12428 ]);
12429 let result = evaluator.evaluate(&expr, &data).unwrap();
12430 // Whole number literals are preserved as integers
12431 let mut expected = IndexMap::new();
12432 expected.insert("name".to_string(), JValue::string("Alice"));
12433 expected.insert("age".to_string(), JValue::Number(30.0));
12434 assert_eq!(result, JValue::object(expected));
12435 }
12436
12437 #[test]
12438 fn test_conditional() {
12439 let mut evaluator = Evaluator::new();
12440 let data = JValue::Null;
12441
12442 // True condition
12443 let expr = AstNode::Conditional {
12444 condition: Box::new(AstNode::boolean(true)),
12445 then_branch: Box::new(AstNode::string("yes")),
12446 else_branch: Some(Box::new(AstNode::string("no"))),
12447 };
12448 let result = evaluator.evaluate(&expr, &data).unwrap();
12449 assert_eq!(result, JValue::string("yes"));
12450
12451 // False condition
12452 let expr = AstNode::Conditional {
12453 condition: Box::new(AstNode::boolean(false)),
12454 then_branch: Box::new(AstNode::string("yes")),
12455 else_branch: Some(Box::new(AstNode::string("no"))),
12456 };
12457 let result = evaluator.evaluate(&expr, &data).unwrap();
12458 assert_eq!(result, JValue::string("no"));
12459
12460 // No else branch returns undefined (not null)
12461 let expr = AstNode::Conditional {
12462 condition: Box::new(AstNode::boolean(false)),
12463 then_branch: Box::new(AstNode::string("yes")),
12464 else_branch: None,
12465 };
12466 let result = evaluator.evaluate(&expr, &data).unwrap();
12467 assert_eq!(result, JValue::Undefined);
12468 }
12469
12470 #[test]
12471 fn test_block_expression() {
12472 let mut evaluator = Evaluator::new();
12473 let data = JValue::Null;
12474
12475 let expr = AstNode::Block(vec![
12476 AstNode::number(1.0),
12477 AstNode::number(2.0),
12478 AstNode::number(3.0),
12479 ]);
12480 let result = evaluator.evaluate(&expr, &data).unwrap();
12481 // Block returns the last expression; whole numbers are preserved as integers
12482 assert_eq!(result, JValue::from(3i64));
12483 }
12484
12485 #[test]
12486 fn test_function_calls() {
12487 let mut evaluator = Evaluator::new();
12488 let data = JValue::Null;
12489
12490 // uppercase function
12491 let expr = AstNode::Function {
12492 name: "uppercase".to_string(),
12493 args: vec![AstNode::string("hello")],
12494 is_builtin: true,
12495 };
12496 let result = evaluator.evaluate(&expr, &data).unwrap();
12497 assert_eq!(result, JValue::string("HELLO"));
12498
12499 // lowercase function
12500 let expr = AstNode::Function {
12501 name: "lowercase".to_string(),
12502 args: vec![AstNode::string("HELLO")],
12503 is_builtin: true,
12504 };
12505 let result = evaluator.evaluate(&expr, &data).unwrap();
12506 assert_eq!(result, JValue::string("hello"));
12507
12508 // length function
12509 let expr = AstNode::Function {
12510 name: "length".to_string(),
12511 args: vec![AstNode::string("hello")],
12512 is_builtin: true,
12513 };
12514 let result = evaluator.evaluate(&expr, &data).unwrap();
12515 assert_eq!(result, JValue::from(5i64));
12516
12517 // sum function
12518 let expr = AstNode::Function {
12519 name: "sum".to_string(),
12520 args: vec![AstNode::Array(vec![
12521 AstNode::number(1.0),
12522 AstNode::number(2.0),
12523 AstNode::number(3.0),
12524 ])],
12525 is_builtin: true,
12526 };
12527 let result = evaluator.evaluate(&expr, &data).unwrap();
12528 assert_eq!(result, JValue::Number(6.0));
12529
12530 // count function
12531 let expr = AstNode::Function {
12532 name: "count".to_string(),
12533 args: vec![AstNode::Array(vec![
12534 AstNode::number(1.0),
12535 AstNode::number(2.0),
12536 AstNode::number(3.0),
12537 ])],
12538 is_builtin: true,
12539 };
12540 let result = evaluator.evaluate(&expr, &data).unwrap();
12541 assert_eq!(result, JValue::from(3i64));
12542 }
12543
12544 #[test]
12545 fn test_complex_nested_data() {
12546 let mut evaluator = Evaluator::new();
12547 let data = JValue::from(serde_json::json!({
12548 "users": [
12549 {"name": "Alice", "age": 30},
12550 {"name": "Bob", "age": 25},
12551 {"name": "Charlie", "age": 35}
12552 ],
12553 "metadata": {
12554 "total": 3,
12555 "version": "1.0"
12556 }
12557 }));
12558 // Access nested field
12559 let path = AstNode::Path {
12560 steps: vec![
12561 PathStep::new(AstNode::Name("metadata".to_string())),
12562 PathStep::new(AstNode::Name("version".to_string())),
12563 ],
12564 };
12565 let result = evaluator.evaluate(&path, &data).unwrap();
12566 assert_eq!(result, JValue::string("1.0"));
12567 }
12568
12569 #[test]
12570 fn test_error_handling() {
12571 let mut evaluator = Evaluator::new();
12572 let data = JValue::Null;
12573
12574 // Type error: adding string and number
12575 let expr = AstNode::Binary {
12576 op: BinaryOp::Add,
12577 lhs: Box::new(AstNode::string("hello")),
12578 rhs: Box::new(AstNode::number(5.0)),
12579 };
12580 let result = evaluator.evaluate(&expr, &data);
12581 assert!(result.is_err());
12582
12583 // Reference error: undefined function
12584 let expr = AstNode::Function {
12585 name: "undefined_function".to_string(),
12586 args: vec![],
12587 is_builtin: false,
12588 };
12589 let result = evaluator.evaluate(&expr, &data);
12590 assert!(result.is_err());
12591 }
12592
12593 #[test]
12594 fn test_truthiness() {
12595 let evaluator = Evaluator::new();
12596
12597 assert!(!evaluator.is_truthy(&JValue::Null));
12598 assert!(!evaluator.is_truthy(&JValue::Bool(false)));
12599 assert!(evaluator.is_truthy(&JValue::Bool(true)));
12600 assert!(!evaluator.is_truthy(&JValue::from(0i64)));
12601 assert!(evaluator.is_truthy(&JValue::from(1i64)));
12602 assert!(!evaluator.is_truthy(&JValue::string("")));
12603 assert!(evaluator.is_truthy(&JValue::string("hello")));
12604 assert!(!evaluator.is_truthy(&JValue::array(vec![])));
12605 assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
12606 }
12607
12608 #[test]
12609 fn test_integration_with_parser() {
12610 use crate::parser::parse;
12611
12612 let mut evaluator = Evaluator::new();
12613 let data = JValue::from(serde_json::json!({
12614 "price": 10,
12615 "quantity": 5
12616 }));
12617 // Test simple path
12618 let ast = parse("price").unwrap();
12619 let result = evaluator.evaluate(&ast, &data).unwrap();
12620 assert_eq!(result, JValue::from(10i64));
12621
12622 // Test arithmetic
12623 let ast = parse("price * quantity").unwrap();
12624 let result = evaluator.evaluate(&ast, &data).unwrap();
12625 // Note: Arithmetic operations produce f64 results in JSON
12626 assert_eq!(result, JValue::Number(50.0));
12627
12628 // Test comparison
12629 let ast = parse("price > 5").unwrap();
12630 let result = evaluator.evaluate(&ast, &data).unwrap();
12631 assert_eq!(result, JValue::Bool(true));
12632 }
12633
12634 #[test]
12635 fn test_evaluate_dollar_function_uppercase() {
12636 use crate::parser::parse;
12637
12638 let mut evaluator = Evaluator::new();
12639 let ast = parse(r#"$uppercase("hello")"#).unwrap();
12640 let empty = JValue::object(IndexMap::new());
12641 let result = evaluator.evaluate(&ast, &empty).unwrap();
12642 assert_eq!(result, JValue::string("HELLO"));
12643 }
12644
12645 #[test]
12646 fn test_evaluate_dollar_function_sum() {
12647 use crate::parser::parse;
12648
12649 let mut evaluator = Evaluator::new();
12650 let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
12651 let empty = JValue::object(IndexMap::new());
12652 let result = evaluator.evaluate(&ast, &empty).unwrap();
12653 assert_eq!(result, JValue::Number(15.0));
12654 }
12655
12656 #[test]
12657 fn test_evaluate_nested_dollar_functions() {
12658 use crate::parser::parse;
12659
12660 let mut evaluator = Evaluator::new();
12661 let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
12662 let empty = JValue::object(IndexMap::new());
12663 let result = evaluator.evaluate(&ast, &empty).unwrap();
12664 // length() returns an integer, not a float
12665 assert_eq!(result, JValue::Number(5.0));
12666 }
12667
12668 #[test]
12669 fn test_array_mapping() {
12670 use crate::parser::parse;
12671
12672 let mut evaluator = Evaluator::new();
12673 let data: JValue = serde_json::from_str(
12674 r#"{
12675 "products": [
12676 {"id": 1, "name": "Laptop", "price": 999.99},
12677 {"id": 2, "name": "Mouse", "price": 29.99},
12678 {"id": 3, "name": "Keyboard", "price": 79.99}
12679 ]
12680 }"#,
12681 )
12682 .map(|v: serde_json::Value| JValue::from(v))
12683 .unwrap();
12684
12685 // Test mapping over array to extract field
12686 let ast = parse("products.name").unwrap();
12687 let result = evaluator.evaluate(&ast, &data).unwrap();
12688 assert_eq!(
12689 result,
12690 JValue::array(vec![
12691 JValue::string("Laptop"),
12692 JValue::string("Mouse"),
12693 JValue::string("Keyboard")
12694 ])
12695 );
12696
12697 // Test mapping over array to extract prices
12698 let ast = parse("products.price").unwrap();
12699 let result = evaluator.evaluate(&ast, &data).unwrap();
12700 assert_eq!(
12701 result,
12702 JValue::array(vec![
12703 JValue::Number(999.99),
12704 JValue::Number(29.99),
12705 JValue::Number(79.99)
12706 ])
12707 );
12708
12709 // Test with $sum function on mapped array
12710 let ast = parse("$sum(products.price)").unwrap();
12711 let result = evaluator.evaluate(&ast, &data).unwrap();
12712 assert_eq!(result, JValue::Number(1109.97));
12713 }
12714
12715 #[test]
12716 fn test_empty_brackets() {
12717 use crate::parser::parse;
12718
12719 let mut evaluator = Evaluator::new();
12720
12721 // Test empty brackets on simple value - should wrap in array
12722 let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
12723 let ast = parse("foo[]").unwrap();
12724 let result = evaluator.evaluate(&ast, &data).unwrap();
12725 assert_eq!(
12726 result,
12727 JValue::array(vec![JValue::string("bar")]),
12728 "Empty brackets should wrap value in array"
12729 );
12730
12731 // Test empty brackets on array - should return array as-is
12732 let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
12733 let ast2 = parse("arr[]").unwrap();
12734 let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
12735 assert_eq!(
12736 result2,
12737 JValue::array(vec![
12738 JValue::Number(1.0),
12739 JValue::Number(2.0),
12740 JValue::Number(3.0)
12741 ]),
12742 "Empty brackets should preserve array"
12743 );
12744 }
12745
12746 // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
12747 // Expected values below are ground-truthed against jsonata-js 2.x.
12748
12749 #[test]
12750 fn test_index_bind_makes_variable_available_in_next_step() {
12751 // `#$o` binds each Order's position; `$o` must resolve in the later step.
12752 let data: JValue = serde_json::json!({
12753 "Account": {
12754 "Order": [
12755 {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
12756 {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
12757 ]
12758 }
12759 })
12760 .into();
12761 let ast =
12762 crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
12763 let mut evaluator = Evaluator::new();
12764 let result = evaluator.evaluate(&ast, &data).unwrap();
12765 assert_eq!(
12766 result,
12767 serde_json::json!([
12768 {"name": "Hat", "idx": 0},
12769 {"name": "Cap", "idx": 1},
12770 {"name": "Sock", "idx": 1}
12771 ])
12772 .into()
12773 );
12774 }
12775
12776 #[test]
12777 fn test_index_bind_with_predicate_stage() {
12778 // Mirrors reference joins/index[13]: index binding, then a predicate on
12779 // the next step, carrying the index binding through.
12780 let data: JValue = serde_json::json!({
12781 "Account": {
12782 "Order": [
12783 {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
12784 {"Product": [{"ProductID": 9, "Name": "C"}]}
12785 ]
12786 }
12787 })
12788 .into();
12789 let ast =
12790 crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
12791 .unwrap();
12792 let mut evaluator = Evaluator::new();
12793 let result = evaluator.evaluate(&ast, &data).unwrap();
12794 assert_eq!(
12795 result,
12796 serde_json::json!([
12797 {"n": "B", "idx": 0},
12798 {"n": "C", "idx": 1}
12799 ])
12800 .into()
12801 );
12802 }
12803
12804 #[test]
12805 fn test_focus_bind_makes_variable_available_in_next_step() {
12806 // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
12807 // does NOT advance the context `@`); the variable itself is what carries
12808 // forward. This asserts the real jsonata-js behaviour.
12809 let data: JValue = serde_json::json!({
12810 "Account": {
12811 "Order": [
12812 {"OrderID": "o1"},
12813 {"OrderID": "o2"}
12814 ]
12815 }
12816 })
12817 .into();
12818 let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
12819 let mut evaluator = Evaluator::new();
12820 let result = evaluator.evaluate(&ast, &data).unwrap();
12821 assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
12822 }
12823
12824 #[test]
12825 fn test_parent_reference_resolves_to_enclosing_step_value() {
12826 let data: JValue = serde_json::json!({
12827 "Account": {
12828 "Order": [
12829 {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
12830 ]
12831 }
12832 })
12833 .into();
12834 let ast =
12835 crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
12836 .unwrap();
12837 let mut evaluator = Evaluator::new();
12838 let result = evaluator.evaluate(&ast, &data).unwrap();
12839 assert_eq!(
12840 result,
12841 serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
12842 );
12843 }
12844
12845 // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
12846 // a tuple-carried `$name`/`!label` key straight into the top scope and then
12847 // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
12848 // same-named outer `:=` binding that happened to be live in that scope
12849 // frame. Expected values below are verified against jsonata-js (2.2.1
12850 // reference, `tests/jsonata-js`).
12851
12852 #[test]
12853 fn test_chained_focus_bind_does_not_clobber_outer_variable() {
12854 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12855 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
12856 let mut evaluator = Evaluator::new();
12857 let result = evaluator.evaluate(&ast, &data).unwrap();
12858 assert_eq!(result, serde_json::json!("OUT").into());
12859 }
12860
12861 #[test]
12862 fn test_chained_index_bind_does_not_clobber_outer_variable() {
12863 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12864 let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
12865 let mut evaluator = Evaluator::new();
12866 let result = evaluator.evaluate(&ast, &data).unwrap();
12867 assert_eq!(result, serde_json::json!("OUT").into());
12868 }
12869
12870 #[test]
12871 fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
12872 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12873 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
12874 let mut evaluator = Evaluator::new();
12875 let result = evaluator.evaluate(&ast, &data).unwrap();
12876 assert_eq!(result, serde_json::json!("OUT").into());
12877 }
12878
12879 #[test]
12880 fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
12881 let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
12882 let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
12883 let mut evaluator = Evaluator::new();
12884 let result = evaluator.evaluate(&ast, &data).unwrap();
12885 assert_eq!(result, serde_json::json!("OUT").into());
12886 }
12887}