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