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 #[cfg(feature = "python")]
976 JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field)?),
977 _ => Ok(JValue::Undefined),
978 },
979
980 CompiledExpr::NestedFieldLookup(outer, inner) => match data {
981 JValue::Object(obj) => {
982 // Shape-accelerated outer lookup
983 let outer_val = if let Some(shape) = shape {
984 if let Some(&idx) = shape.get(outer.as_str()) {
985 obj.get_index(idx).map(|(_, v)| v)
986 } else {
987 obj.get(outer.as_str())
988 }
989 } else {
990 obj.get(outer.as_str())
991 };
992 match outer_val {
993 Some(JValue::Object(nested)) => Ok(nested
994 .get(inner.as_str())
995 .cloned()
996 .unwrap_or(JValue::Undefined)),
997 #[cfg(feature = "python")]
998 Some(JValue::LazyPyDict(nested)) => Ok(nested.get_field(inner.as_str())?),
999 _ => Ok(JValue::Undefined),
1000 }
1001 }
1002 #[cfg(feature = "python")]
1003 JValue::LazyPyDict(lazy) => {
1004 let outer_val = lazy.get_field(outer.as_str())?;
1005 match outer_val {
1006 JValue::Object(nested) => Ok(nested
1007 .get(inner.as_str())
1008 .cloned()
1009 .unwrap_or(JValue::Undefined)),
1010 JValue::LazyPyDict(nested) => Ok(nested.get_field(inner.as_str())?),
1011 _ => Ok(JValue::Undefined),
1012 }
1013 }
1014 _ => Ok(JValue::Undefined),
1015 },
1016
1017 CompiledExpr::VariableLookup(var) => {
1018 if let Some(vars) = vars {
1019 if let Some(val) = vars.get(var.as_str()) {
1020 return Ok((*val).clone());
1021 }
1022 }
1023 // $ (empty var name) refers to the current data
1024 if var.is_empty() {
1025 return Ok(data.clone());
1026 }
1027 Ok(JValue::Undefined)
1028 }
1029
1030 // ── Comparison ──────────────────────────────────────────────────
1031 CompiledExpr::Compare { op, lhs, rhs } => {
1032 let lhs_explicit_null = is_compiled_explicit_null(lhs);
1033 let rhs_explicit_null = is_compiled_explicit_null(rhs);
1034 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1035 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1036 match op {
1037 // compiled_equal normalizes lazy operands (guarded, zero-cost when
1038 // neither side is lazy) so conversion failures raise instead of
1039 // silently comparing unequal.
1040 CompiledCmp::Eq => compiled_equal(&left, &right),
1041 CompiledCmp::Ne => match compiled_equal(&left, &right)? {
1042 JValue::Bool(b) => Ok(JValue::Bool(!b)),
1043 other => Ok(other),
1044 },
1045 CompiledCmp::Lt => compiled_ordered_cmp(
1046 &left,
1047 &right,
1048 lhs_explicit_null,
1049 rhs_explicit_null,
1050 |a, b| a < b,
1051 |a, b| a < b,
1052 ),
1053 CompiledCmp::Le => compiled_ordered_cmp(
1054 &left,
1055 &right,
1056 lhs_explicit_null,
1057 rhs_explicit_null,
1058 |a, b| a <= b,
1059 |a, b| a <= b,
1060 ),
1061 CompiledCmp::Gt => compiled_ordered_cmp(
1062 &left,
1063 &right,
1064 lhs_explicit_null,
1065 rhs_explicit_null,
1066 |a, b| a > b,
1067 |a, b| a > b,
1068 ),
1069 CompiledCmp::Ge => compiled_ordered_cmp(
1070 &left,
1071 &right,
1072 lhs_explicit_null,
1073 rhs_explicit_null,
1074 |a, b| a >= b,
1075 |a, b| a >= b,
1076 ),
1077 }
1078 }
1079
1080 // ── Arithmetic ──────────────────────────────────────────────────
1081 CompiledExpr::Arithmetic { op, lhs, rhs } => {
1082 let lhs_explicit_null = is_compiled_explicit_null(lhs);
1083 let rhs_explicit_null = is_compiled_explicit_null(rhs);
1084 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1085 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1086 compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
1087 }
1088
1089 // ── String concat ───────────────────────────────────────────────
1090 CompiledExpr::Concat(lhs, rhs) => {
1091 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1092 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1093 let ls = compiled_to_concat_string(&left)?;
1094 let rs = compiled_to_concat_string(&right)?;
1095 Ok(JValue::string(format!("{}{}", ls, rs)))
1096 }
1097
1098 // ── Logical ─────────────────────────────────────────────────────
1099 CompiledExpr::And(lhs, rhs) => {
1100 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1101 if !compiled_is_truthy(&left) {
1102 return Ok(JValue::Bool(false));
1103 }
1104 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1105 Ok(JValue::Bool(compiled_is_truthy(&right)))
1106 }
1107 CompiledExpr::Or(lhs, rhs) => {
1108 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1109 if compiled_is_truthy(&left) {
1110 return Ok(JValue::Bool(true));
1111 }
1112 let right = eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)?;
1113 Ok(JValue::Bool(compiled_is_truthy(&right)))
1114 }
1115 CompiledExpr::Not(inner) => {
1116 let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1117 Ok(JValue::Bool(!compiled_is_truthy(&val)))
1118 }
1119 CompiledExpr::Negate(inner) => {
1120 let val = eval_compiled_inner(inner, data, vars, ctx, shape, options, start_time)?;
1121 match val {
1122 JValue::Number(n) => Ok(JValue::Number(-n)),
1123 JValue::Null => Ok(JValue::Null),
1124 // Undefined operand propagates through unary minus, matching the tree-walker.
1125 v if v.is_undefined() => Ok(JValue::Undefined),
1126 _ => Err(EvaluatorError::TypeError(
1127 "D1002: Cannot negate non-number value".to_string(),
1128 )),
1129 }
1130 }
1131
1132 // ── Conditional ─────────────────────────────────────────────────
1133 CompiledExpr::Conditional {
1134 condition,
1135 then_expr,
1136 else_expr,
1137 } => {
1138 let cond = eval_compiled_inner(condition, data, vars, ctx, shape, options, start_time)?;
1139 if compiled_is_truthy(&cond) {
1140 eval_compiled_inner(then_expr, data, vars, ctx, shape, options, start_time)
1141 } else if let Some(else_e) = else_expr {
1142 eval_compiled_inner(else_e, data, vars, ctx, shape, options, start_time)
1143 } else {
1144 Ok(JValue::Undefined)
1145 }
1146 }
1147
1148 // ── Object construction ─────────────────────────────────────────
1149 CompiledExpr::ObjectConstruct(fields) => {
1150 let mut result = IndexMap::with_capacity(fields.len());
1151 for (key, expr) in fields {
1152 let value = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1153 if !value.is_undefined() {
1154 result.insert(key.clone(), value);
1155 }
1156 }
1157 Ok(JValue::object(result))
1158 }
1159
1160 // ── Array construction ──────────────────────────────────────────
1161 CompiledExpr::ArrayConstruct(elems) => {
1162 let mut result = Vec::new();
1163 for (elem_expr, is_nested) in elems {
1164 let value =
1165 eval_compiled_inner(elem_expr, data, vars, ctx, shape, options, start_time)?;
1166 // Undefined values are excluded from array constructors (tree-walker parity)
1167 if value.is_undefined() {
1168 continue;
1169 }
1170 if *is_nested {
1171 // Explicit array constructor [...] — keep nested even if it's an array
1172 result.push(value);
1173 } else if let JValue::Array(arr) = value {
1174 // Non-constructor that evaluated to an array — flatten one level
1175 result.extend(arr.iter().cloned());
1176 } else {
1177 result.push(value);
1178 }
1179 }
1180 Ok(JValue::array(result))
1181 }
1182
1183 // ── Phase 2 new variants ─────────────────────────────────────────
1184
1185 // ContextVar: named variable lookup from context scope.
1186 // In top-level mode (ctx=None, no bindings), returns Undefined.
1187 // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
1188 // is only ever populated for top-level calls — always Undefined there.
1189 CompiledExpr::ContextVar(name) => {
1190 // Check vars map first (for lambda params that might shadow context)
1191 if let Some(vars) = vars {
1192 if let Some(val) = vars.get(name.as_str()) {
1193 return Ok((*val).clone());
1194 }
1195 }
1196 // Then check context scope
1197 if let Some(ctx) = ctx {
1198 if let Some(val) = ctx.lookup(name) {
1199 return Ok(val.clone());
1200 }
1201 }
1202 Ok(JValue::Undefined)
1203 }
1204
1205 // FieldPath: multi-step field access with implicit array mapping.
1206 CompiledExpr::FieldPath(steps) => {
1207 compiled_eval_field_path(steps, data, vars, ctx, shape, options, start_time)
1208 }
1209
1210 // BuiltinCall: evaluate all args, dispatch to pure builtin.
1211 CompiledExpr::BuiltinCall { name, args } => {
1212 let mut evaled_args = Vec::with_capacity(args.len());
1213 for arg in args.iter() {
1214 evaled_args.push(eval_compiled_inner(
1215 arg, data, vars, ctx, shape, options, start_time,
1216 )?);
1217 }
1218 call_pure_builtin(name, &evaled_args, data, options)
1219 }
1220
1221 // Block: evaluate each expression in sequence, return the last value.
1222 CompiledExpr::Block(exprs) => {
1223 let mut result = JValue::Undefined;
1224 for expr in exprs.iter() {
1225 result = eval_compiled_inner(expr, data, vars, ctx, shape, options, start_time)?;
1226 }
1227 Ok(result)
1228 }
1229
1230 // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1231 // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1232 CompiledExpr::Coalesce(lhs, rhs) => {
1233 let left = eval_compiled_inner(lhs, data, vars, ctx, shape, options, start_time)?;
1234 if left.is_undefined() {
1235 eval_compiled_inner(rhs, data, vars, ctx, shape, options, start_time)
1236 } else {
1237 Ok(left)
1238 }
1239 }
1240
1241 // ── Higher-order functions ─────────────────────────────────────────────
1242 //
1243 // These variants are emitted by try_compile_hof_expr when the HOF argument
1244 // is an inline lambda literal with a compilable body. Outer vars are merged
1245 // with the lambda params so that nested HOF can access variables from
1246 // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1247 CompiledExpr::MapCall {
1248 array,
1249 params,
1250 body,
1251 } => {
1252 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1253 let single_holder;
1254 let items: &[JValue] = match &arr_val {
1255 JValue::Array(a) => a.as_slice(),
1256 JValue::Undefined => return Ok(JValue::Undefined),
1257 other => {
1258 single_holder = [other.clone()];
1259 &single_holder[..]
1260 }
1261 };
1262 let mut result = Vec::with_capacity(items.len());
1263 let p0 = params.first().map(|s| s.as_str());
1264
1265 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1266 // 2-param lambda (element + index): build per-iteration because idx_val
1267 // is loop-local and cannot outlive the iteration.
1268 for (idx, item) in items.iter().enumerate() {
1269 check_loop_timeout(options, start_time)?;
1270 let idx_val = JValue::Number(idx as f64);
1271 let mut call_vars = clone_outer_vars(vars, 2);
1272 if let Some(p) = p0 {
1273 call_vars.insert(p, item);
1274 }
1275 call_vars.insert(p1, &idx_val);
1276 let mapped = eval_compiled_inner(
1277 body,
1278 data,
1279 Some(&call_vars),
1280 ctx,
1281 shape,
1282 options,
1283 start_time,
1284 )?;
1285 if !mapped.is_undefined() {
1286 result.push(mapped);
1287 }
1288 }
1289 } else if let Some(p0) = p0 {
1290 // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1291 let mut call_vars = clone_outer_vars(vars, 1);
1292 for item in items.iter() {
1293 check_loop_timeout(options, start_time)?;
1294 call_vars.insert(p0, item);
1295 let mapped = eval_compiled_inner(
1296 body,
1297 data,
1298 Some(&call_vars),
1299 ctx,
1300 shape,
1301 options,
1302 start_time,
1303 )?;
1304 if !mapped.is_undefined() {
1305 result.push(mapped);
1306 }
1307 }
1308 }
1309 check_sequence_length(result.len(), options)?;
1310 Ok(if result.is_empty() {
1311 JValue::Undefined
1312 } else {
1313 JValue::array(result)
1314 })
1315 }
1316
1317 CompiledExpr::FilterCall {
1318 array,
1319 params,
1320 body,
1321 } => {
1322 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1323 if arr_val.is_undefined() || arr_val.is_null() {
1324 return Ok(JValue::Undefined);
1325 }
1326 let single_holder;
1327 let (items, was_single) = match &arr_val {
1328 JValue::Array(a) => (a.as_slice(), false),
1329 other => {
1330 single_holder = [other.clone()];
1331 (&single_holder[..], true)
1332 }
1333 };
1334 let mut result = Vec::with_capacity(items.len() / 2);
1335 let p0 = params.first().map(|s| s.as_str());
1336
1337 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1338 for (idx, item) in items.iter().enumerate() {
1339 check_loop_timeout(options, start_time)?;
1340 let idx_val = JValue::Number(idx as f64);
1341 let mut call_vars = clone_outer_vars(vars, 2);
1342 if let Some(p) = p0 {
1343 call_vars.insert(p, item);
1344 }
1345 call_vars.insert(p1, &idx_val);
1346 let pred = eval_compiled_inner(
1347 body,
1348 data,
1349 Some(&call_vars),
1350 ctx,
1351 shape,
1352 options,
1353 start_time,
1354 )?;
1355 if compiled_is_truthy(&pred) {
1356 result.push(item.clone());
1357 }
1358 }
1359 } else if let Some(p0) = p0 {
1360 let mut call_vars = clone_outer_vars(vars, 1);
1361 for item in items.iter() {
1362 check_loop_timeout(options, start_time)?;
1363 call_vars.insert(p0, item);
1364 let pred = eval_compiled_inner(
1365 body,
1366 data,
1367 Some(&call_vars),
1368 ctx,
1369 shape,
1370 options,
1371 start_time,
1372 )?;
1373 if compiled_is_truthy(&pred) {
1374 result.push(item.clone());
1375 }
1376 }
1377 }
1378 if was_single {
1379 Ok(match result.len() {
1380 0 => JValue::Undefined,
1381 1 => {
1382 check_sequence_length(1, options)?;
1383 result.remove(0)
1384 }
1385 _ => {
1386 check_sequence_length(result.len(), options)?;
1387 JValue::array(result)
1388 }
1389 })
1390 } else {
1391 check_sequence_length(result.len(), options)?;
1392 Ok(JValue::array(result))
1393 }
1394 }
1395
1396 CompiledExpr::ReduceCall {
1397 array,
1398 params,
1399 body,
1400 initial,
1401 } => {
1402 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape, options, start_time)?;
1403 let single_holder;
1404 let items: &[JValue] = match &arr_val {
1405 JValue::Array(a) => a.as_slice(),
1406 JValue::Null => return Ok(JValue::Null),
1407 JValue::Undefined => return Ok(JValue::Undefined),
1408 other => {
1409 single_holder = [other.clone()];
1410 &single_holder[..]
1411 }
1412 };
1413 let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1414 let init_val =
1415 eval_compiled_inner(init_expr, data, vars, ctx, shape, options, start_time)?;
1416 if items.is_empty() {
1417 return Ok(init_val);
1418 }
1419 (0usize, init_val)
1420 } else {
1421 if items.is_empty() {
1422 return Ok(JValue::Null);
1423 }
1424 (1, items[0].clone())
1425 };
1426 let acc_param = params[0].as_str();
1427 let item_param = params[1].as_str();
1428 for item in items[start_idx..].iter() {
1429 check_loop_timeout(options, start_time)?;
1430 // Per-iteration HashMap: &accumulator borrow must be released before we
1431 // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1432 let mut call_vars = clone_outer_vars(vars, 2);
1433 call_vars.insert(acc_param, &accumulator);
1434 call_vars.insert(item_param, item);
1435 let new_acc = eval_compiled_inner(
1436 body,
1437 data,
1438 Some(&call_vars),
1439 ctx,
1440 shape,
1441 options,
1442 start_time,
1443 )?;
1444 drop(call_vars);
1445 accumulator = new_acc;
1446 }
1447 Ok(accumulator)
1448 }
1449 }
1450}
1451
1452/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1453#[inline]
1454pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1455 match value {
1456 JValue::Null | JValue::Undefined => false,
1457 JValue::Bool(b) => *b,
1458 JValue::Number(n) => *n != 0.0,
1459 JValue::String(s) => !s.is_empty(),
1460 JValue::Array(a) => !a.is_empty(),
1461 JValue::Object(o) => !o.is_empty(),
1462 _ => false,
1463 }
1464}
1465
1466/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1467/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1468#[inline]
1469fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1470 matches!(expr, CompiledExpr::ExplicitNull)
1471}
1472
1473/// Ordered comparison for compiled expressions.
1474/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1475#[inline]
1476pub(crate) fn compiled_ordered_cmp(
1477 left: &JValue,
1478 right: &JValue,
1479 left_is_explicit_null: bool,
1480 right_is_explicit_null: bool,
1481 cmp_num: fn(f64, f64) -> bool,
1482 cmp_str: fn(&str, &str) -> bool,
1483) -> Result<JValue, EvaluatorError> {
1484 match (left, right) {
1485 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1486 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1487 // Both null/undefined → undefined
1488 (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1489 (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1490 // Explicit null literal with any non-null type → T2010 error
1491 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1492 "T2010: Type mismatch in comparison".to_string(),
1493 )),
1494 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1495 "T2010: Type mismatch in comparison".to_string(),
1496 )),
1497 // Boolean with undefined → T2010 error
1498 (JValue::Bool(_), JValue::Null | JValue::Undefined)
1499 | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1500 EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1501 ),
1502 // Number or String with implicit undefined (missing field) → undefined result
1503 (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1504 | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1505 Ok(JValue::Null)
1506 }
1507 // Type mismatch (string vs number)
1508 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1509 Err(EvaluatorError::EvaluationError(
1510 "T2009: The expressions on either side of operator must be of the same data type"
1511 .to_string(),
1512 ))
1513 }
1514 _ => Err(EvaluatorError::EvaluationError(
1515 "T2010: Type mismatch in comparison".to_string(),
1516 )),
1517 }
1518}
1519
1520/// Arithmetic for compiled expressions.
1521/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1522#[inline]
1523pub(crate) fn compiled_arithmetic(
1524 op: CompiledArithOp,
1525 left: &JValue,
1526 right: &JValue,
1527 left_is_explicit_null: bool,
1528 right_is_explicit_null: bool,
1529) -> Result<JValue, EvaluatorError> {
1530 let op_sym = match op {
1531 CompiledArithOp::Add => "+",
1532 CompiledArithOp::Sub => "-",
1533 CompiledArithOp::Mul => "*",
1534 CompiledArithOp::Div => "/",
1535 CompiledArithOp::Mod => "%",
1536 };
1537 match (left, right) {
1538 (JValue::Number(a), JValue::Number(b)) => {
1539 let result = match op {
1540 CompiledArithOp::Add => *a + *b,
1541 CompiledArithOp::Sub => *a - *b,
1542 CompiledArithOp::Mul => {
1543 let r = *a * *b;
1544 if r.is_infinite() {
1545 return Err(EvaluatorError::EvaluationError(
1546 "D1001: Number out of range".to_string(),
1547 ));
1548 }
1549 r
1550 }
1551 CompiledArithOp::Div => {
1552 if *b == 0.0 {
1553 return Err(EvaluatorError::EvaluationError(
1554 "Division by zero".to_string(),
1555 ));
1556 }
1557 *a / *b
1558 }
1559 CompiledArithOp::Mod => {
1560 if *b == 0.0 {
1561 return Err(EvaluatorError::EvaluationError(
1562 "Division by zero".to_string(),
1563 ));
1564 }
1565 *a % *b
1566 }
1567 };
1568 Ok(JValue::Number(result))
1569 }
1570 // Explicit null literal → T2002 error (matching tree-walker behavior)
1571 (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1572 Err(EvaluatorError::TypeError(format!(
1573 "T2002: The left side of the {} operator must evaluate to a number",
1574 op_sym
1575 )))
1576 }
1577 (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1578 Err(EvaluatorError::TypeError(format!(
1579 "T2002: The right side of the {} operator must evaluate to a number",
1580 op_sym
1581 )))
1582 }
1583 // Implicit undefined propagation (from missing field) → undefined result
1584 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1585 Ok(JValue::Null)
1586 }
1587 _ => Err(EvaluatorError::TypeError(format!(
1588 "Cannot apply {} to {:?} and {:?}",
1589 op_sym, left, right
1590 ))),
1591 }
1592}
1593
1594/// Convert a value to string for concatenation in compiled expressions.
1595#[inline]
1596pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1597 // Normalize a lazy operand up front: `functions::string::string`'s lazy arm maps a
1598 // conversion failure to `JValue::Null` (silently stringifying to `""`), which would
1599 // swallow the TypeError this must raise instead. Guarded by `is_lazy` so the common
1600 // (non-lazy) path pays no clone.
1601 let normalized;
1602 let value = if value.is_lazy() {
1603 normalized = normalize_lazy(value)?;
1604 &normalized
1605 } else {
1606 value
1607 };
1608 match value {
1609 JValue::String(s) => Ok(s.to_string()),
1610 JValue::Null | JValue::Undefined => Ok(String::new()),
1611 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1612 match crate::functions::string::string(value, None) {
1613 Ok(JValue::String(s)) => Ok(s.to_string()),
1614 Ok(JValue::Null) => Ok(String::new()),
1615 _ => Err(EvaluatorError::TypeError(
1616 "Cannot concatenate complex types".to_string(),
1617 )),
1618 }
1619 }
1620 _ => Ok(String::new()),
1621 }
1622}
1623
1624/// Equality comparison for the bytecode VM.
1625#[inline]
1626pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> Result<JValue, EvaluatorError> {
1627 // Normalize lazy operands so a conversion failure raises TypeError here rather than
1628 // being swallowed as `false` by `values_equal`'s lazy `to_object_ref` arms. Guarded
1629 // by `is_lazy` so the common (non-lazy) path pays no clone.
1630 if lhs.is_lazy() || rhs.is_lazy() {
1631 let lhs = normalize_lazy(lhs)?;
1632 let rhs = normalize_lazy(rhs)?;
1633 return Ok(JValue::Bool(crate::functions::array::values_equal(
1634 &lhs, &rhs,
1635 )));
1636 }
1637 Ok(JValue::Bool(crate::functions::array::values_equal(
1638 lhs, rhs,
1639 )))
1640}
1641
1642/// String concatenation for the bytecode VM.
1643#[inline]
1644pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1645 let l = compiled_to_concat_string(&lhs)?;
1646 let r = compiled_to_concat_string(&rhs)?;
1647 Ok(JValue::string(l + &r))
1648}
1649
1650/// Entry point for the bytecode VM to call pure builtins by name.
1651#[inline]
1652pub(crate) fn call_pure_builtin_by_name(
1653 name: &str,
1654 args: &[JValue],
1655 data: &JValue,
1656 options: &EvaluatorOptions,
1657) -> Result<JValue, EvaluatorError> {
1658 call_pure_builtin(name, args, data, options)
1659}
1660
1661// ──────────────────────────────────────────────────────────────────────────────
1662// Phase 2: path compilation, builtin dispatch, and supporting helpers
1663// ──────────────────────────────────────────────────────────────────────────────
1664
1665/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1666///
1667/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1668/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1669fn try_compile_path(
1670 steps: &[crate::ast::PathStep],
1671 allowed_vars: Option<&[&str]>,
1672) -> Option<CompiledExpr> {
1673 use crate::ast::{AstNode, Stage};
1674
1675 if steps.is_empty() {
1676 return None;
1677 }
1678
1679 // Determine the start of the path:
1680 // `$.field...` → starts from current data (drop the leading `$` step)
1681 // `$var.field` → variable-prefixed paths: not compiled yet, fall back to tree-walker
1682 // `field...` → starts from current data
1683 let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1684 AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1685 AstNode::Variable(_) => return None,
1686 AstNode::Name(_) => steps,
1687 _ => return None,
1688 };
1689
1690 // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1691 // which represent index access in JSONata, not boolean filtering, and the
1692 // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1693 // to stay an array rather than filtering — the tree-walker's
1694 // `evaluate_predicate` special-cases it and the compiled path has no
1695 // equivalent, so bail out rather than silently treating it as `filter(true)`.
1696 let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1697 if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1698 return None;
1699 }
1700 try_compile_expr_inner(node, allowed_vars)
1701 };
1702
1703 // Compile each field step.
1704 // Handles:
1705 // - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1706 // - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1707 // previous step's filter slot, since both encodings have identical runtime semantics.
1708 let mut compiled_steps = Vec::with_capacity(field_steps.len());
1709 for step in field_steps {
1710 // Tuple-stream steps (@ focus / # index / % parent binding) require the
1711 // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1712 // tuple handling). Never compile them to the flat bytecode field path,
1713 // which is unaware of the binding flags and would silently drop them.
1714 if step.focus.is_some()
1715 || step.index_var.is_some()
1716 || step.ancestor_label.is_some()
1717 || step.is_tuple
1718 {
1719 return None;
1720 }
1721 match &step.node {
1722 AstNode::Name(name) => {
1723 let filter = match step.stages.as_slice() {
1724 [] => None,
1725 [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1726 _ => return None,
1727 };
1728 compiled_steps.push(CompiledStep {
1729 field: name.clone(),
1730 filter,
1731 });
1732 }
1733 AstNode::Predicate(filter_node) => {
1734 // Standalone predicate step — fold into the previous Name step's filter slot.
1735 if !step.stages.is_empty() {
1736 return None;
1737 }
1738 let last = compiled_steps.last_mut()?;
1739 if last.filter.is_some() {
1740 return None;
1741 }
1742 last.filter = Some(compile_filter(filter_node)?);
1743 }
1744 _ => return None,
1745 }
1746 }
1747
1748 if compiled_steps.is_empty() {
1749 // Bare `$` with no further field steps — current-data reference
1750 return Some(CompiledExpr::VariableLookup(String::new()));
1751 }
1752
1753 // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1754 // in HOF mode (allowed_vars=Some), where data is always a single Object element
1755 // from an array. In top-level mode (allowed_vars=None), data can itself be an
1756 // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1757 if allowed_vars.is_some() {
1758 if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1759 return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1760 }
1761 if compiled_steps.len() == 2
1762 && compiled_steps[0].filter.is_none()
1763 && compiled_steps[1].filter.is_none()
1764 {
1765 let outer = compiled_steps.remove(0).field;
1766 let inner = compiled_steps.remove(0).field;
1767 return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1768 }
1769 }
1770
1771 Some(CompiledExpr::FieldPath(compiled_steps))
1772}
1773
1774/// Evaluate a compiled `FieldPath` against `data`.
1775///
1776/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1777/// Filters are applied as predicates: truthy elements are kept.
1778///
1779/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1780/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1781/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1782fn compiled_eval_field_path(
1783 steps: &[CompiledStep],
1784 data: &JValue,
1785 vars: Option<&HashMap<&str, &JValue>>,
1786 ctx: Option<&Context>,
1787 shape: Option<&ShapeCache>,
1788 options: &EvaluatorOptions,
1789 start_time: Option<Instant>,
1790) -> Result<JValue, EvaluatorError> {
1791 let mut current = data.clone();
1792 // Track whether the most recent field step mapped over an array (like the tree-walker's
1793 // `did_array_mapping` flag). Filters also count as array operations.
1794 let mut did_array_mapping = false;
1795 for step in steps {
1796 // Determine if this step will do array mapping before we overwrite `current`
1797 let is_array = matches!(current, JValue::Array(_));
1798 // Field access with implicit array mapping
1799 current = compiled_field_step(&step.field, ¤t, options)?;
1800 if is_array {
1801 did_array_mapping = true;
1802 } else {
1803 // Extracting from a single object resets the flag (tree-walker parity)
1804 did_array_mapping = false;
1805 }
1806 // Apply filter if present (filter is an array operation — keep the flag set)
1807 if let Some(filter) = &step.filter {
1808 current =
1809 compiled_apply_filter(filter, ¤t, vars, ctx, shape, options, start_time)?;
1810 // Filter always implies we operated on an array
1811 did_array_mapping = true;
1812 }
1813 }
1814 // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1815 if did_array_mapping {
1816 Ok(match current {
1817 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1818 other => other,
1819 })
1820 } else {
1821 Ok(current)
1822 }
1823}
1824
1825/// Perform a single-field access with implicit array-mapping semantics.
1826///
1827/// - Object: look up `field`, return its value or Undefined
1828/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1829/// (this is a query-result sequence, so D2015 applies — mirrors `evaluate_path`'s
1830/// array-mapping check and `vm.rs`'s `get_field_cached`)
1831/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1832/// - Other: Undefined
1833fn compiled_field_step(
1834 field: &str,
1835 value: &JValue,
1836 options: &EvaluatorOptions,
1837) -> Result<JValue, EvaluatorError> {
1838 match value {
1839 JValue::Object(obj) => {
1840 // Check for tuple: extract from "@" inner object
1841 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1842 match obj.get("@") {
1843 Some(JValue::Object(inner)) => {
1844 return Ok(inner.get(field).cloned().unwrap_or(JValue::Undefined));
1845 }
1846 #[cfg(feature = "python")]
1847 Some(JValue::LazyPyDict(lazy)) => {
1848 return Ok(lazy.get_field(field)?);
1849 }
1850 _ => {}
1851 }
1852 return Ok(JValue::Undefined);
1853 }
1854 Ok(obj.get(field).cloned().unwrap_or(JValue::Undefined))
1855 }
1856 #[cfg(feature = "python")]
1857 JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field)?),
1858 JValue::Array(arr) => {
1859 // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1860 let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1861 if let JValue::Object(obj) = v {
1862 if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1863 return build_shape_cache(v);
1864 }
1865 }
1866 None
1867 });
1868 let mut result = Vec::new();
1869 for item in arr.iter() {
1870 let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1871 // Tuple objects need the recursive path for "@" inner lookup.
1872 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1873 compiled_field_step(field, item, options)?
1874 } else if let Some(&pos) = sh.get(field) {
1875 // Positional access with key verification: guards against heterogeneous
1876 // schemas (objects where the same field is at a different index).
1877 // On a mismatch, fall back to a regular hash lookup.
1878 match obj.get_index(pos) {
1879 Some((k, v)) if k.as_str() == field => v.clone(),
1880 _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1881 }
1882 } else {
1883 // Field not in the first object's schema — fall back to hash lookup
1884 // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1885 obj.get(field).cloned().unwrap_or(JValue::Undefined)
1886 }
1887 } else {
1888 compiled_field_step(field, item, options)?
1889 };
1890 match extracted {
1891 JValue::Undefined => {}
1892 JValue::Array(inner) => result.extend(inner.iter().cloned()),
1893 other => result.push(other),
1894 }
1895 }
1896 check_sequence_length(result.len(), options)?;
1897 Ok(if result.is_empty() {
1898 JValue::Undefined
1899 } else {
1900 JValue::array(result)
1901 })
1902 }
1903 _ => Ok(JValue::Undefined),
1904 }
1905}
1906
1907/// Apply a compiled filter predicate to a value.
1908///
1909/// - Array: return elements for which the predicate is truthy
1910/// - Single value: return it if predicate is truthy, else Undefined
1911/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1912fn compiled_apply_filter(
1913 filter: &CompiledExpr,
1914 value: &JValue,
1915 vars: Option<&HashMap<&str, &JValue>>,
1916 ctx: Option<&Context>,
1917 shape: Option<&ShapeCache>,
1918 options: &EvaluatorOptions,
1919 start_time: Option<Instant>,
1920) -> Result<JValue, EvaluatorError> {
1921 match value {
1922 JValue::Array(arr) => {
1923 let mut result = Vec::new();
1924 // Auto-build shape cache from first element when not provided.
1925 // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1926 let local_shape: Option<ShapeCache> = if shape.is_none() {
1927 arr.first().and_then(build_shape_cache)
1928 } else {
1929 None
1930 };
1931 let effective_shape = shape.or(local_shape.as_ref());
1932 for item in arr.iter() {
1933 check_loop_timeout(options, start_time)?;
1934 let pred = eval_compiled_inner(
1935 filter,
1936 item,
1937 vars,
1938 ctx,
1939 effective_shape,
1940 options,
1941 start_time,
1942 )?;
1943 if compiled_is_truthy(&pred) {
1944 result.push(item.clone());
1945 }
1946 }
1947 if result.is_empty() {
1948 Ok(JValue::Undefined)
1949 } else if result.len() == 1 {
1950 check_sequence_length(1, options)?;
1951 Ok(result.remove(0))
1952 } else {
1953 check_sequence_length(result.len(), options)?;
1954 Ok(JValue::array(result))
1955 }
1956 }
1957 JValue::Undefined => Ok(JValue::Undefined),
1958 _ => {
1959 let pred = eval_compiled_inner(filter, value, vars, ctx, shape, options, start_time)?;
1960 if compiled_is_truthy(&pred) {
1961 Ok(value.clone())
1962 } else {
1963 Ok(JValue::Undefined)
1964 }
1965 }
1966 }
1967}
1968
1969/// Materialize a top-level lazy dict into a plain Object. Non-lazy values
1970/// pass through unchanged. Does NOT recurse into arrays/objects — element-
1971/// level laziness is handled by the specific consumers that need it.
1972#[cfg(feature = "python")]
1973pub(crate) fn normalize_lazy(value: &JValue) -> Result<JValue, EvaluatorError> {
1974 match value {
1975 JValue::LazyPyDict(lazy) => Ok(JValue::Object(lazy.to_object()?)),
1976 _ => Ok(value.clone()),
1977 }
1978}
1979
1980#[cfg(not(feature = "python"))]
1981pub(crate) fn normalize_lazy(value: &JValue) -> Result<JValue, EvaluatorError> {
1982 Ok(value.clone())
1983}
1984
1985/// Dispatch a pure builtin function call.
1986///
1987/// Replicates the tree-walker's evaluation for the subset of builtins in
1988/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1989/// `data` is the current context value for implicit-argument insertion.
1990fn call_pure_builtin(
1991 name: &str,
1992 args: &[JValue],
1993 data: &JValue,
1994 options: &EvaluatorOptions,
1995) -> Result<JValue, EvaluatorError> {
1996 use crate::functions;
1997
1998 // Apply implicit context insertion matching the tree-walker
1999 let args_storage: Vec<JValue>;
2000 let effective_args: &[JValue] = if args.is_empty() {
2001 match name {
2002 "string" => {
2003 // $string() with a null/undefined context returns undefined, not "null".
2004 // This mirrors the tree-walker's special case at the function-call site.
2005 if data.is_undefined() || data.is_null() {
2006 return Ok(JValue::Undefined);
2007 }
2008 args_storage = vec![data.clone()];
2009 &args_storage
2010 }
2011 "number" | "boolean" | "uppercase" | "lowercase" => {
2012 args_storage = vec![data.clone()];
2013 &args_storage
2014 }
2015 _ => args,
2016 }
2017 } else if args.len() == 1 {
2018 match name {
2019 "substringBefore" | "substringAfter" | "contains" | "split" => {
2020 if matches!(data, JValue::String(_)) {
2021 args_storage = std::iter::once(data.clone())
2022 .chain(args.iter().cloned())
2023 .collect();
2024 &args_storage
2025 } else {
2026 args
2027 }
2028 }
2029 _ => args,
2030 }
2031 } else {
2032 args
2033 };
2034
2035 // Materialize top-level lazy args so every builtin sees plain Objects.
2036 #[cfg(feature = "python")]
2037 let lazy_storage: Vec<JValue>;
2038 #[cfg(feature = "python")]
2039 let effective_args: &[JValue] = if effective_args
2040 .iter()
2041 .any(|a| matches!(a, JValue::LazyPyDict(_)))
2042 {
2043 lazy_storage = effective_args
2044 .iter()
2045 .map(normalize_lazy)
2046 .collect::<Result<Vec<_>, _>>()?;
2047 &lazy_storage
2048 } else {
2049 effective_args
2050 };
2051
2052 // Apply undefined propagation: if the first effective argument is Undefined
2053 // and the function propagates undefined, return Undefined immediately.
2054 // This matches the tree-walker's `propagates_undefined` check.
2055 if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
2056 return Ok(JValue::Undefined);
2057 }
2058
2059 match name {
2060 // ── String functions ────────────────────────────────────────────
2061 "string" => {
2062 // Validate the optional prettify argument: must be a boolean.
2063 let prettify = match effective_args.get(1) {
2064 None => None,
2065 Some(JValue::Bool(b)) => Some(*b),
2066 Some(_) => {
2067 return Err(EvaluatorError::TypeError(
2068 "string() prettify parameter must be a boolean".to_string(),
2069 ))
2070 }
2071 };
2072 let arg = effective_args.first().unwrap_or(&JValue::Null);
2073 Ok(functions::string::string(arg, prettify)?)
2074 }
2075 "length" => match effective_args.first() {
2076 Some(JValue::String(s)) => Ok(functions::string::length(s)?),
2077 // Undefined input propagates (caught above by the undefined-propagation guard).
2078 Some(JValue::Undefined) => Ok(JValue::Undefined),
2079 // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
2080 // so the test framework accepts it against any expected T-code).
2081 None => Err(EvaluatorError::EvaluationError(
2082 "length() requires exactly 1 argument".to_string(),
2083 )),
2084 // null and any other non-string type → T0410
2085 _ => Err(EvaluatorError::TypeError(
2086 "T0410: Argument 1 of function length does not match function signature"
2087 .to_string(),
2088 )),
2089 },
2090 "uppercase" => match effective_args.first() {
2091 Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
2092 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2093 _ => Err(EvaluatorError::TypeError(
2094 "T0410: Argument 1 of function uppercase does not match function signature"
2095 .to_string(),
2096 )),
2097 },
2098 "lowercase" => match effective_args.first() {
2099 Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
2100 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
2101 _ => Err(EvaluatorError::TypeError(
2102 "T0410: Argument 1 of function lowercase does not match function signature"
2103 .to_string(),
2104 )),
2105 },
2106 "trim" => match effective_args.first() {
2107 None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
2108 Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
2109 _ => Err(EvaluatorError::TypeError(
2110 "trim() requires a string argument".to_string(),
2111 )),
2112 },
2113 "substring" => {
2114 if effective_args.len() < 2 {
2115 return Err(EvaluatorError::EvaluationError(
2116 "substring() requires at least 2 arguments".to_string(),
2117 ));
2118 }
2119 match (&effective_args[0], &effective_args[1]) {
2120 (JValue::String(s), JValue::Number(start)) => {
2121 // Optional 3rd arg (length) must be a number if provided.
2122 let length = match effective_args.get(2) {
2123 None => None,
2124 Some(JValue::Number(l)) => Some(*l as i64),
2125 Some(_) => {
2126 return Err(EvaluatorError::TypeError(
2127 "T0410: Argument 3 of function substring does not match function signature"
2128 .to_string(),
2129 ))
2130 }
2131 };
2132 Ok(functions::string::substring(s, *start as i64, length)?)
2133 }
2134 _ => Err(EvaluatorError::TypeError(
2135 "T0410: Argument 1 of function substring does not match function signature"
2136 .to_string(),
2137 )),
2138 }
2139 }
2140 "substringBefore" => {
2141 if effective_args.len() != 2 {
2142 return Err(EvaluatorError::TypeError(
2143 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
2144 ));
2145 }
2146 match (&effective_args[0], &effective_args[1]) {
2147 (JValue::String(s), JValue::String(sep)) => {
2148 Ok(functions::string::substring_before(s, sep)?)
2149 }
2150 // Undefined propagates; null is a type error.
2151 (JValue::Undefined, _) => Ok(JValue::Undefined),
2152 _ => Err(EvaluatorError::TypeError(
2153 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
2154 )),
2155 }
2156 }
2157 "substringAfter" => {
2158 if effective_args.len() != 2 {
2159 return Err(EvaluatorError::TypeError(
2160 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
2161 ));
2162 }
2163 match (&effective_args[0], &effective_args[1]) {
2164 (JValue::String(s), JValue::String(sep)) => {
2165 Ok(functions::string::substring_after(s, sep)?)
2166 }
2167 // Undefined propagates; null is a type error.
2168 (JValue::Undefined, _) => Ok(JValue::Undefined),
2169 _ => Err(EvaluatorError::TypeError(
2170 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
2171 )),
2172 }
2173 }
2174 "contains" => {
2175 if effective_args.len() != 2 {
2176 return Err(EvaluatorError::EvaluationError(
2177 "contains() requires exactly 2 arguments".to_string(),
2178 ));
2179 }
2180 match &effective_args[0] {
2181 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2182 JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
2183 _ => Err(EvaluatorError::TypeError(
2184 "contains() requires a string as the first argument".to_string(),
2185 )),
2186 }
2187 }
2188 "split" => {
2189 if effective_args.len() < 2 {
2190 return Err(EvaluatorError::EvaluationError(
2191 "split() requires at least 2 arguments".to_string(),
2192 ));
2193 }
2194 match &effective_args[0] {
2195 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2196 JValue::String(s) => {
2197 // Validate the optional limit argument — must be a positive number.
2198 let limit = match effective_args.get(2) {
2199 None => None,
2200 Some(JValue::Number(n)) => {
2201 if *n < 0.0 {
2202 return Err(EvaluatorError::EvaluationError(
2203 "D3020: Third argument of split function must be a positive number"
2204 .to_string(),
2205 ));
2206 }
2207 Some(n.floor() as usize)
2208 }
2209 Some(_) => {
2210 return Err(EvaluatorError::TypeError(
2211 "split() limit must be a number".to_string(),
2212 ))
2213 }
2214 };
2215 Ok(functions::string::split(s, &effective_args[1], limit)?)
2216 }
2217 _ => Err(EvaluatorError::TypeError(
2218 "split() requires a string as the first argument".to_string(),
2219 )),
2220 }
2221 }
2222 "join" => {
2223 if effective_args.is_empty() {
2224 return Err(EvaluatorError::TypeError(
2225 "T0410: Argument 1 of function $join does not match function signature"
2226 .to_string(),
2227 ));
2228 }
2229 match &effective_args[0] {
2230 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2231 // Signature: <a<s>s?:s> — first arg must be an array of strings.
2232 JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
2233 Err(EvaluatorError::TypeError(
2234 "T0412: Argument 1 of function $join must be an array of String"
2235 .to_string(),
2236 ))
2237 }
2238 #[cfg(feature = "python")]
2239 JValue::LazyPyDict(_) => Err(EvaluatorError::TypeError(
2240 "T0412: Argument 1 of function $join must be an array of String".to_string(),
2241 )),
2242 JValue::Array(arr) => {
2243 // All elements must be strings.
2244 for item in arr.iter() {
2245 if !matches!(item, JValue::String(_)) {
2246 return Err(EvaluatorError::TypeError(
2247 "T0412: Argument 1 of function $join must be an array of String"
2248 .to_string(),
2249 ));
2250 }
2251 }
2252 // Validate separator: must be a string if provided.
2253 let separator = match effective_args.get(1) {
2254 None | Some(JValue::Undefined) => None,
2255 Some(JValue::String(s)) => Some(&**s),
2256 Some(_) => {
2257 return Err(EvaluatorError::TypeError(
2258 "T0410: Argument 2 of function $join does not match function signature (expected String)"
2259 .to_string(),
2260 ))
2261 }
2262 };
2263 Ok(functions::string::join(arr, separator)?)
2264 }
2265 JValue::String(s) => Ok(JValue::String(s.clone())),
2266 _ => Err(EvaluatorError::TypeError(
2267 "T0412: Argument 1 of function $join must be an array of String".to_string(),
2268 )),
2269 }
2270 }
2271
2272 // ── Numeric functions ───────────────────────────────────────────
2273 "number" => match effective_args.first() {
2274 Some(v) => Ok(functions::numeric::number(v)?),
2275 None => Err(EvaluatorError::EvaluationError(
2276 "number() requires at least 1 argument".to_string(),
2277 )),
2278 },
2279 "floor" => match effective_args.first() {
2280 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2281 Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
2282 _ => Err(EvaluatorError::TypeError(
2283 "floor() requires a number argument".to_string(),
2284 )),
2285 },
2286 "ceil" => match effective_args.first() {
2287 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2288 Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
2289 _ => Err(EvaluatorError::TypeError(
2290 "ceil() requires a number argument".to_string(),
2291 )),
2292 },
2293 "round" => match effective_args.first() {
2294 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2295 Some(JValue::Number(n)) => {
2296 let precision = effective_args.get(1).and_then(|v| {
2297 if let JValue::Number(p) = v {
2298 Some(*p as i32)
2299 } else {
2300 None
2301 }
2302 });
2303 Ok(functions::numeric::round(*n, precision)?)
2304 }
2305 _ => Err(EvaluatorError::TypeError(
2306 "round() requires a number argument".to_string(),
2307 )),
2308 },
2309 "abs" => match effective_args.first() {
2310 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2311 Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
2312 _ => Err(EvaluatorError::TypeError(
2313 "abs() requires a number argument".to_string(),
2314 )),
2315 },
2316 "sqrt" => match effective_args.first() {
2317 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2318 Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
2319 _ => Err(EvaluatorError::TypeError(
2320 "sqrt() requires a number argument".to_string(),
2321 )),
2322 },
2323
2324 // ── Aggregation functions ───────────────────────────────────────
2325 "sum" => match effective_args.first() {
2326 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2327 None => Err(EvaluatorError::EvaluationError(
2328 "sum() requires exactly 1 argument".to_string(),
2329 )),
2330 Some(JValue::Null) => Ok(JValue::Null),
2331 Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
2332 Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
2333 Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
2334 },
2335 "max" => match effective_args.first() {
2336 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2337 Some(JValue::Null) | None => Ok(JValue::Null),
2338 Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
2339 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2340 _ => Err(EvaluatorError::TypeError(
2341 "max() requires an array or number argument".to_string(),
2342 )),
2343 },
2344 "min" => match effective_args.first() {
2345 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2346 Some(JValue::Null) | None => Ok(JValue::Null),
2347 Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
2348 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2349 _ => Err(EvaluatorError::TypeError(
2350 "min() requires an array or number argument".to_string(),
2351 )),
2352 },
2353 "average" => match effective_args.first() {
2354 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2355 Some(JValue::Null) | None => Ok(JValue::Null),
2356 Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2357 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2358 _ => Err(EvaluatorError::TypeError(
2359 "average() requires an array or number argument".to_string(),
2360 )),
2361 },
2362 "count" => match effective_args.first() {
2363 Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2364 Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2365 Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2366 _ => Ok(JValue::from(1i64)),
2367 },
2368
2369 // ── Boolean / logic ─────────────────────────────────────────────
2370 "boolean" => match effective_args.first() {
2371 Some(v) => Ok(functions::boolean::boolean(v)?),
2372 None => Err(EvaluatorError::EvaluationError(
2373 "boolean() requires 1 argument".to_string(),
2374 )),
2375 },
2376 "not" => match effective_args.first() {
2377 Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2378 None => Err(EvaluatorError::EvaluationError(
2379 "not() requires 1 argument".to_string(),
2380 )),
2381 },
2382
2383 // ── Array functions ─────────────────────────────────────────────
2384 "append" => {
2385 if effective_args.len() != 2 {
2386 return Err(EvaluatorError::EvaluationError(
2387 "append() requires exactly 2 arguments".to_string(),
2388 ));
2389 }
2390 let first = &effective_args[0];
2391 let second = &effective_args[1];
2392 if matches!(second, JValue::Null | JValue::Undefined) {
2393 return Ok(first.clone());
2394 }
2395 if matches!(first, JValue::Null | JValue::Undefined) {
2396 return Ok(second.clone());
2397 }
2398 let arr = match first {
2399 JValue::Array(a) => a.to_vec(),
2400 other => vec![other.clone()],
2401 };
2402 let second_len = match second {
2403 JValue::Array(a) => a.len(),
2404 _ => 1,
2405 };
2406 check_sequence_length(arr.len() + second_len, options)?;
2407 Ok(functions::array::append(&arr, second)?)
2408 }
2409 "reverse" => match effective_args.first() {
2410 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2411 Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2412 _ => Err(EvaluatorError::TypeError(
2413 "reverse() requires an array argument".to_string(),
2414 )),
2415 },
2416 "distinct" => match effective_args.first() {
2417 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2418 Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2419 // Non-array input, and arrays of length <= 1, pass through unchanged
2420 // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2421 Some(other) => Ok(other.clone()),
2422 },
2423
2424 // ── Object functions ────────────────────────────────────────────
2425 "keys" => match effective_args.first() {
2426 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2427 Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2428 Some(JValue::Object(obj)) => {
2429 if obj.is_empty() {
2430 Ok(JValue::Null)
2431 } else {
2432 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2433 check_sequence_length(keys.len(), options)?;
2434 if keys.len() == 1 {
2435 Ok(keys.into_iter().next().unwrap())
2436 } else {
2437 Ok(JValue::array(keys))
2438 }
2439 }
2440 }
2441 Some(JValue::Array(arr)) => {
2442 let mut all_keys: Vec<JValue> = Vec::new();
2443 for item in arr.iter() {
2444 let normalized_item = normalize_lazy(item)?;
2445 if let JValue::Object(obj) = &normalized_item {
2446 for key in obj.keys() {
2447 let k = JValue::string(key.clone());
2448 if !all_keys.contains(&k) {
2449 all_keys.push(k);
2450 }
2451 }
2452 }
2453 }
2454 if all_keys.is_empty() {
2455 Ok(JValue::Null)
2456 } else if all_keys.len() == 1 {
2457 Ok(all_keys.into_iter().next().unwrap())
2458 } else {
2459 check_sequence_length(all_keys.len(), options)?;
2460 Ok(JValue::array(all_keys))
2461 }
2462 }
2463 _ => Ok(JValue::Null),
2464 },
2465 "merge" => match effective_args.len() {
2466 0 => Err(EvaluatorError::EvaluationError(
2467 "merge() requires at least 1 argument".to_string(),
2468 )),
2469 1 => match &effective_args[0] {
2470 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2471 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2472 JValue::Object(_) => Ok(effective_args[0].clone()),
2473 _ => Err(EvaluatorError::TypeError(
2474 "merge() requires objects or an array of objects".to_string(),
2475 )),
2476 },
2477 _ => Ok(functions::object::merge(effective_args)?),
2478 },
2479
2480 _ => unreachable!(
2481 "call_pure_builtin called with non-compilable builtin: {}",
2482 name
2483 ),
2484 }
2485}
2486
2487// ──────────────────────────────────────────────────────────────────────────────
2488// End of CompiledExpr framework
2489// ──────────────────────────────────────────────────────────────────────────────
2490
2491/// Functions that propagate undefined (return undefined when given an undefined argument).
2492/// These functions should return null/undefined when their input path doesn't exist,
2493/// rather than throwing a type error.
2494const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2495 "not",
2496 "boolean",
2497 "length",
2498 "number",
2499 "uppercase",
2500 "lowercase",
2501 "substring",
2502 "substringBefore",
2503 "substringAfter",
2504 "string",
2505 "abs",
2506 "ceil",
2507 "floor",
2508 "round",
2509 "sqrt",
2510];
2511
2512/// Check whether a function propagates undefined values
2513fn propagates_undefined(name: &str) -> bool {
2514 UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2515}
2516
2517/// Iterator-based numeric aggregation helpers.
2518/// These avoid cloning values by iterating over references and extracting f64 values directly.
2519mod aggregation {
2520 use super::*;
2521
2522 /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2523 /// Returns Err if any non-numeric value is encountered.
2524 fn for_each_numeric(
2525 arr: &[JValue],
2526 func_name: &str,
2527 mut f: impl FnMut(f64),
2528 ) -> Result<(), EvaluatorError> {
2529 fn recurse(
2530 arr: &[JValue],
2531 func_name: &str,
2532 f: &mut dyn FnMut(f64),
2533 ) -> Result<(), EvaluatorError> {
2534 for value in arr.iter() {
2535 match value {
2536 JValue::Array(inner) => recurse(inner, func_name, f)?,
2537 JValue::Number(n) => {
2538 f(*n);
2539 }
2540 _ => {
2541 return Err(EvaluatorError::TypeError(format!(
2542 "{}() requires all array elements to be numbers",
2543 func_name
2544 )));
2545 }
2546 }
2547 }
2548 Ok(())
2549 }
2550 recurse(arr, func_name, &mut f)
2551 }
2552
2553 /// Count elements in a potentially nested array without cloning.
2554 fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2555 let mut count = 0usize;
2556 for_each_numeric(arr, func_name, |_| count += 1)?;
2557 Ok(count)
2558 }
2559
2560 pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2561 if arr.is_empty() {
2562 return Ok(JValue::from(0i64));
2563 }
2564 let mut total = 0.0f64;
2565 for_each_numeric(arr, "sum", |n| total += n)?;
2566 Ok(JValue::Number(total))
2567 }
2568
2569 pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2570 if arr.is_empty() {
2571 return Ok(JValue::Null);
2572 }
2573 let mut max_val = f64::NEG_INFINITY;
2574 for_each_numeric(arr, "max", |n| {
2575 if n > max_val {
2576 max_val = n;
2577 }
2578 })?;
2579 Ok(JValue::Number(max_val))
2580 }
2581
2582 pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2583 if arr.is_empty() {
2584 return Ok(JValue::Null);
2585 }
2586 let mut min_val = f64::INFINITY;
2587 for_each_numeric(arr, "min", |n| {
2588 if n < min_val {
2589 min_val = n;
2590 }
2591 })?;
2592 Ok(JValue::Number(min_val))
2593 }
2594
2595 pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2596 if arr.is_empty() {
2597 return Ok(JValue::Null);
2598 }
2599 let mut total = 0.0f64;
2600 let count = count_numeric(arr, "average")?;
2601 for_each_numeric(arr, "average", |n| total += n)?;
2602 Ok(JValue::Number(total / count as f64))
2603 }
2604}
2605
2606/// Evaluator errors
2607#[derive(Error, Debug)]
2608pub enum EvaluatorError {
2609 #[error("Type error: {0}")]
2610 TypeError(String),
2611
2612 #[error("Reference error: {0}")]
2613 ReferenceError(String),
2614
2615 #[error("Evaluation error: {0}")]
2616 EvaluationError(String),
2617
2618 /// Python→JValue conversion failed during lazy field access.
2619 /// Surfaces as Python TypeError at the boundary (matching what eager
2620 /// conversion would have raised at call time).
2621 #[cfg(feature = "python")]
2622 #[error("Type error: {0}")]
2623 PyConversionError(String),
2624}
2625
2626impl From<crate::functions::FunctionError> for EvaluatorError {
2627 fn from(e: crate::functions::FunctionError) -> Self {
2628 // `PyConversionError` must surface as a Python `TypeError` (matching what
2629 // eager conversion would have raised), not the generic `ValueError` every
2630 // other `FunctionError` variant maps to below -- see its doc comment.
2631 #[cfg(feature = "python")]
2632 if let crate::functions::FunctionError::PyConversionError(m) = &e {
2633 return EvaluatorError::PyConversionError(m.clone());
2634 }
2635 EvaluatorError::EvaluationError(e.to_string())
2636 }
2637}
2638
2639impl From<crate::datetime::DateTimeError> for EvaluatorError {
2640 fn from(e: crate::datetime::DateTimeError) -> Self {
2641 EvaluatorError::EvaluationError(e.to_string())
2642 }
2643}
2644
2645impl EvaluatorError {
2646 /// The underlying message, without the outer "Type error: "/
2647 /// "Reference error: "/"Evaluation error: " prefix that `Display` (via
2648 /// thiserror's `#[error("Type error: {0}")]` etc.) would add. This is
2649 /// what JSONata-spec-coded messages like "T2002: ..." actually look
2650 /// like — the coded prefix is INSIDE this string, not added by
2651 /// `Display`. Used by both the Python bindings (`src/lib.rs`) and the
2652 /// `jsonata` CLI so the two never need to duplicate this unwrap.
2653 pub fn message(&self) -> &str {
2654 match self {
2655 EvaluatorError::TypeError(m) => m,
2656 EvaluatorError::ReferenceError(m) => m,
2657 EvaluatorError::EvaluationError(m) => m,
2658 #[cfg(feature = "python")]
2659 EvaluatorError::PyConversionError(m) => m,
2660 }
2661 }
2662}
2663
2664#[cfg(feature = "python")]
2665impl From<crate::lazy::LazyConvertError> for EvaluatorError {
2666 fn from(e: crate::lazy::LazyConvertError) -> Self {
2667 EvaluatorError::PyConversionError(e.0)
2668 }
2669}
2670
2671#[cfg(test)]
2672mod evaluator_error_message_tests {
2673 use super::EvaluatorError;
2674
2675 #[test]
2676 fn message_strips_the_display_prefix() {
2677 let e = EvaluatorError::TypeError(
2678 "T2002: The left side of the + operator must evaluate to a number".to_string(),
2679 );
2680 assert_eq!(
2681 e.message(),
2682 "T2002: The left side of the + operator must evaluate to a number"
2683 );
2684 // Display, by contrast, adds the "Type error: " wrapper -- this is
2685 // exactly the distinction `message()` exists to avoid.
2686 assert_eq!(
2687 e.to_string(),
2688 "Type error: T2002: The left side of the + operator must evaluate to a number"
2689 );
2690 }
2691
2692 #[test]
2693 fn message_works_for_all_variants() {
2694 assert_eq!(
2695 EvaluatorError::ReferenceError("$foo is not defined".to_string()).message(),
2696 "$foo is not defined"
2697 );
2698 assert_eq!(
2699 EvaluatorError::EvaluationError("something went wrong".to_string()).message(),
2700 "something went wrong"
2701 );
2702 }
2703}
2704
2705/// Result of evaluating a lambda body that may be a tail call
2706/// Used for trampoline-based tail call optimization
2707enum LambdaResult {
2708 /// Final value - evaluation is complete
2709 JValue(JValue),
2710 /// Tail call - need to continue with another lambda invocation
2711 TailCall {
2712 /// The lambda to call (boxed to reduce enum size)
2713 lambda: Box<StoredLambda>,
2714 /// Arguments for the call
2715 args: Vec<JValue>,
2716 /// Data context for the call
2717 data: JValue,
2718 },
2719}
2720
2721/// Lambda storage
2722/// Stores the AST of a lambda function along with its parameters, optional signature,
2723/// and captured environment for closures
2724#[derive(Clone, Debug)]
2725pub struct StoredLambda {
2726 pub params: Vec<String>,
2727 pub body: AstNode,
2728 /// Pre-compiled body for use in tight inner loops (HOF fast path).
2729 /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2730 pub(crate) compiled_body: Option<CompiledExpr>,
2731 pub signature: Option<String>,
2732 /// Captured environment bindings for closures
2733 pub captured_env: HashMap<String, JValue>,
2734 /// Captured data context for lexical scoping of bare field names
2735 pub captured_data: Option<JValue>,
2736 /// Whether this lambda's body contains tail calls that can be optimized
2737 pub thunk: bool,
2738}
2739
2740/// A single scope in the scope stack
2741struct Scope {
2742 bindings: HashMap<String, JValue>,
2743 lambdas: HashMap<String, StoredLambda>,
2744}
2745
2746impl Scope {
2747 fn new() -> Self {
2748 Scope {
2749 bindings: HashMap::new(),
2750 lambdas: HashMap::new(),
2751 }
2752 }
2753}
2754
2755/// Evaluation context
2756///
2757/// Holds variable bindings and other state needed during evaluation.
2758/// Uses a scope stack for efficient push/pop instead of clone/restore.
2759pub struct Context {
2760 scope_stack: Vec<Scope>,
2761 parent_data: Option<JValue>,
2762}
2763
2764impl Context {
2765 pub fn new() -> Self {
2766 Context {
2767 scope_stack: vec![Scope::new()],
2768 parent_data: None,
2769 }
2770 }
2771
2772 /// Push a new scope onto the stack
2773 fn push_scope(&mut self) {
2774 self.scope_stack.push(Scope::new());
2775 }
2776
2777 /// Pop the top scope from the stack
2778 fn pop_scope(&mut self) {
2779 if self.scope_stack.len() > 1 {
2780 self.scope_stack.pop();
2781 }
2782 }
2783
2784 /// Pop scope but preserve specified lambdas by moving them to the current top scope
2785 fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2786 if self.scope_stack.len() > 1 {
2787 let popped = self.scope_stack.pop().unwrap();
2788 if !lambda_ids.is_empty() {
2789 let top = self.scope_stack.last_mut().unwrap();
2790 for id in lambda_ids {
2791 if let Some(stored) = popped.lambdas.get(id) {
2792 top.lambdas.insert(id.clone(), stored.clone());
2793 }
2794 }
2795 }
2796 }
2797 }
2798
2799 /// Clear all bindings and lambdas in the top scope without deallocating
2800 fn clear_current_scope(&mut self) {
2801 let top = self.scope_stack.last_mut().unwrap();
2802 top.bindings.clear();
2803 top.lambdas.clear();
2804 }
2805
2806 pub fn bind(&mut self, name: String, value: JValue) {
2807 self.scope_stack
2808 .last_mut()
2809 .unwrap()
2810 .bindings
2811 .insert(name, value);
2812 }
2813
2814 pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2815 self.scope_stack
2816 .last_mut()
2817 .unwrap()
2818 .lambdas
2819 .insert(name, lambda);
2820 }
2821
2822 pub fn unbind(&mut self, name: &str) {
2823 // Remove from top scope only
2824 let top = self.scope_stack.last_mut().unwrap();
2825 top.bindings.remove(name);
2826 top.lambdas.remove(name);
2827 }
2828
2829 pub fn lookup(&self, name: &str) -> Option<&JValue> {
2830 // Walk scope stack from top to bottom
2831 for scope in self.scope_stack.iter().rev() {
2832 if let Some(value) = scope.bindings.get(name) {
2833 return Some(value);
2834 }
2835 }
2836 None
2837 }
2838
2839 pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2840 // Walk scope stack from top to bottom
2841 for scope in self.scope_stack.iter().rev() {
2842 if let Some(lambda) = scope.lambdas.get(name) {
2843 return Some(lambda);
2844 }
2845 }
2846 None
2847 }
2848
2849 pub fn set_parent(&mut self, data: JValue) {
2850 self.parent_data = Some(data);
2851 }
2852
2853 pub fn get_parent(&self) -> Option<&JValue> {
2854 self.parent_data.as_ref()
2855 }
2856
2857 /// Collect all bindings across all scopes (for environment capture).
2858 /// Higher scopes shadow lower scopes.
2859 fn all_bindings(&self) -> HashMap<String, JValue> {
2860 let mut result = HashMap::new();
2861 for scope in &self.scope_stack {
2862 for (k, v) in &scope.bindings {
2863 result.insert(k.clone(), v.clone());
2864 }
2865 }
2866 result
2867 }
2868}
2869
2870impl Default for Context {
2871 fn default() -> Self {
2872 Self::new()
2873 }
2874}
2875
2876/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2877/// ...}`) from a value about to leave the evaluator.
2878///
2879/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2880/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2881/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2882/// path step consumes and re-wraps these as evaluation proceeds, but the
2883/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2884/// because the tuple-producing expression itself is the whole result (a bare
2885/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2886/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2887/// wrapper ends up embedded in a field value or array element rather than at
2888/// the top level. This recurses through both array elements and (non-tuple)
2889/// object field values so both shapes are cleaned up, not just a bare
2890/// top-level tuple array.
2891/// Merge a group of tuple wrappers into a single tuple, appending each key's
2892/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2893/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2894/// a key present in one tuple stays a scalar; a key present in several becomes an
2895/// array of the collected values (used by group-by value evaluation so a group
2896/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2897/// N collected focus values).
2898fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2899 fn append(acc: Option<JValue>, v: JValue) -> JValue {
2900 match acc {
2901 None => v,
2902 Some(a) => {
2903 let mut out: Vec<JValue> = match a {
2904 JValue::Array(arr) => arr.iter().cloned().collect(),
2905 other => vec![other],
2906 };
2907 match v {
2908 JValue::Array(arr) => out.extend(arr.iter().cloned()),
2909 other => out.push(other),
2910 }
2911 JValue::array(out)
2912 }
2913 }
2914 }
2915 let mut result: IndexMap<String, JValue> = IndexMap::new();
2916 for tuple in group {
2917 if let JValue::Object(obj) = tuple {
2918 for (k, v) in obj.iter() {
2919 if k == "__tuple__" {
2920 result.insert(k.clone(), v.clone());
2921 continue;
2922 }
2923 let merged = append(result.shift_remove(k), v.clone());
2924 result.insert(k.clone(), merged);
2925 }
2926 }
2927 }
2928 result
2929}
2930
2931fn unwrap_tuple_output(value: JValue) -> JValue {
2932 match value {
2933 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2934 .get("@")
2935 .cloned()
2936 .map(unwrap_tuple_output)
2937 .unwrap_or(JValue::Undefined),
2938 JValue::Object(obj) => {
2939 let mut new_map = IndexMap::with_capacity(obj.len());
2940 for (k, v) in obj.iter() {
2941 new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2942 }
2943 JValue::object(new_map)
2944 }
2945 JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2946 other => other,
2947 }
2948}
2949
2950/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2951/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2952/// (if anything) was bound under that name beforehand. `restore` puts the
2953/// prior value back -- or removes the binding entirely if there wasn't
2954/// one -- rather than unconditionally unbinding, so a tuple key that
2955/// happens to share a name with a live outer `:=` binding in the same
2956/// scope frame doesn't get permanently deleted once the tuple-row
2957/// evaluation finishes.
2958struct TupleKeyBindings {
2959 saved: Vec<(String, Option<JValue>)>,
2960}
2961
2962impl TupleKeyBindings {
2963 /// True if `name` was one of the keys this guard bound (used by callers
2964 /// that need to know whether a given tuple key is already in scope
2965 /// before binding it a second time under a different role, e.g.
2966 /// `create_tuple_stream`'s ancestor-label handling).
2967 fn contains(&self, name: &str) -> bool {
2968 self.saved.iter().any(|(n, _)| n == name)
2969 }
2970
2971 fn restore(self, evaluator: &mut Evaluator) {
2972 for (name, prior) in self.saved {
2973 match prior {
2974 Some(value) => evaluator.context.bind(name, value),
2975 None => evaluator.context.unbind(&name),
2976 }
2977 }
2978 }
2979}
2980
2981/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
2982/// evaluator options. All fields default to `None` = unlimited (current behavior).
2983#[derive(Default, Clone, Debug)]
2984pub struct EvaluatorOptions {
2985 /// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
2986 pub timeout_ms: Option<u64>,
2987 /// Maximum AST-recursion stack depth. Exceeding it raises D1011 if this is the
2988 /// tighter of this value and the hardcoded native-stack safety ceiling (302);
2989 /// otherwise the hardcoded ceiling still raises U1001 (see GitHub issue #34).
2990 pub max_stack_depth: Option<usize>,
2991 /// Maximum length of a query-result sequence (map/filter/wildcard/descendants/
2992 /// keys/lookup/append/spread/each/range/path-mapping). Exceeding it raises D2015.
2993 /// Does NOT currently apply to literal array construction (`MakeArray`/
2994 /// `ArrayConstruct`) — NOTE this is a deliberate, temporary divergence from
2995 /// upstream, not a match: jsonata-js DOES cap flat/non-nested array literals
2996 /// (via `fn.append`'s `createSequence` hook in `evaluateUnary`'s `[` case).
2997 /// Deferred until the separate `MakeArray(u16)` truncation bug is fixed (see
2998 /// the design spec's "Sequence length → D2015" section).
2999 pub max_sequence_length: Option<usize>,
3000}
3001
3002/// Checks a constructed query-result sequence's length against the configured
3003/// `max_sequence_length` guardrail. Call this at sites that build a query-result
3004/// sequence (map/filter/wildcard/descendants/keys/lookup/append/spread/each/range/
3005/// path-mapping). NOT currently called at literal array construction (`[1,2,3]`) —
3006/// unlike upstream jsonata-js, which caps flat/non-nested literals too via
3007/// `fn.append`'s `createSequence()` hook. See `EvaluatorOptions::max_sequence_length`
3008/// doc comment above for why this is a deliberate, temporary gap.
3009pub(crate) fn check_sequence_length(
3010 len: usize,
3011 options: &EvaluatorOptions,
3012) -> Result<(), EvaluatorError> {
3013 if let Some(max) = options.max_sequence_length {
3014 if len > max {
3015 return Err(EvaluatorError::EvaluationError(format!(
3016 "D2015: The maximum sequence length of {} was exceeded.",
3017 max
3018 )));
3019 }
3020 }
3021 Ok(())
3022}
3023
3024/// Per-iteration D1012 check for loop-based compiled/VM constructs (map/filter/
3025/// reduce element loops, FilterByBytecode) that don't pass through
3026/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed.
3027#[inline]
3028pub(crate) fn check_loop_timeout(
3029 options: &EvaluatorOptions,
3030 start_time: Option<Instant>,
3031) -> Result<(), EvaluatorError> {
3032 if let Some(timeout_ms) = options.timeout_ms {
3033 if let Some(start) = start_time {
3034 if start.elapsed().as_millis() as u64 > timeout_ms {
3035 return Err(EvaluatorError::EvaluationError(format!(
3036 "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3037 timeout_ms
3038 )));
3039 }
3040 }
3041 }
3042 Ok(())
3043}
3044
3045/// Evaluator for JSONata expressions
3046pub struct Evaluator {
3047 context: Context,
3048 recursion_depth: usize,
3049 max_recursion_depth: usize,
3050 /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
3051 /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
3052 /// using the AST node's pointer address (as before) collided whenever the same
3053 /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
3054 /// or other repeated recursion), aliasing unrelated closures that shared an id.
3055 next_lambda_id: u64,
3056 /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
3057 /// wrapper during this top-level `evaluate()` call. Reset at the start of
3058 /// `evaluate()` and checked at the end to decide whether the (recursive,
3059 /// O(result size)) tuple-unwrap pass is needed before returning to the
3060 /// caller — keeps the vast majority of evaluations, which never touch
3061 /// `%`/`@`/`#`, at zero added cost.
3062 tuple_stream_created: bool,
3063 /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
3064 /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
3065 /// by the two consumers that read those carried bindings directly from the
3066 /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
3067 /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
3068 /// evaluating its input path (key/value expressions read `$focus` off the
3069 /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
3070 /// of projecting each tuple's `@`.
3071 keep_tuple_stream: bool,
3072 options: EvaluatorOptions,
3073 /// Set in `evaluate()` (only when `options.timeout_ms` is configured) and
3074 /// checked in `evaluate_internal`'s per-node checkpoint for D1012.
3075 start_time: Option<Instant>,
3076}
3077
3078impl Evaluator {
3079 pub fn new() -> Self {
3080 Evaluator {
3081 context: Context::new(),
3082 recursion_depth: 0,
3083 // Limit recursion depth to prevent stack overflow
3084 // True TCO would allow deeper recursion but requires parser-level thunk marking
3085 max_recursion_depth: 302,
3086 next_lambda_id: 0,
3087 tuple_stream_created: false,
3088 keep_tuple_stream: false,
3089 options: EvaluatorOptions::default(),
3090 start_time: None,
3091 }
3092 }
3093
3094 pub fn with_context(context: Context) -> Self {
3095 Evaluator {
3096 context,
3097 recursion_depth: 0,
3098 max_recursion_depth: 302,
3099 next_lambda_id: 0,
3100 tuple_stream_created: false,
3101 keep_tuple_stream: false,
3102 options: EvaluatorOptions::default(),
3103 start_time: None,
3104 }
3105 }
3106
3107 /// Construct an `Evaluator` with guardrail options. `Evaluator::new()`/
3108 /// `with_context()` remain unchanged (unlimited options) for existing callers.
3109 pub fn with_options(context: Context, options: EvaluatorOptions) -> Self {
3110 Evaluator {
3111 context,
3112 recursion_depth: 0,
3113 max_recursion_depth: 302,
3114 next_lambda_id: 0,
3115 tuple_stream_created: false,
3116 keep_tuple_stream: false,
3117 options,
3118 start_time: None,
3119 }
3120 }
3121
3122 /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
3123 fn fresh_lambda_id(&mut self) -> u64 {
3124 let id = self.next_lambda_id;
3125 self.next_lambda_id += 1;
3126 id
3127 }
3128
3129 /// Invoke a stored lambda with its captured environment and data.
3130 /// This is the standard way to call a StoredLambda, handling the
3131 /// captured_env and captured_data extraction boilerplate.
3132 fn invoke_stored_lambda(
3133 &mut self,
3134 stored: &StoredLambda,
3135 args: &[JValue],
3136 data: &JValue,
3137 ) -> Result<JValue, EvaluatorError> {
3138 // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
3139 // Conditions: has compiled body, no signature (can't skip validation), no thunk,
3140 // and no captured lambda/builtin values (those require Context for runtime lookup).
3141 if let Some(ref ce) = stored.compiled_body {
3142 if stored.signature.is_none()
3143 && !stored.thunk
3144 && !stored
3145 .captured_env
3146 .values()
3147 .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
3148 {
3149 let call_data = stored.captured_data.as_ref().unwrap_or(data);
3150 let vars: HashMap<&str, &JValue> = stored
3151 .params
3152 .iter()
3153 .zip(args.iter())
3154 .map(|(p, v)| (p.as_str(), v))
3155 .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
3156 .collect();
3157 return eval_compiled(ce, call_data, Some(&vars), &self.options, self.start_time);
3158 }
3159 }
3160
3161 let captured_env = if stored.captured_env.is_empty() {
3162 None
3163 } else {
3164 Some(&stored.captured_env)
3165 };
3166 let captured_data = stored.captured_data.as_ref();
3167 self.invoke_lambda_with_env(
3168 &stored.params,
3169 &stored.body,
3170 stored.signature.as_ref(),
3171 args,
3172 data,
3173 captured_env,
3174 captured_data,
3175 stored.thunk,
3176 )
3177 }
3178
3179 /// Look up a StoredLambda from a JValue that may be a lambda marker.
3180 /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
3181 /// with a valid lambda_id that references a stored lambda.
3182 fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
3183 if let JValue::Lambda { lambda_id, .. } = value {
3184 return self.context.lookup_lambda(lambda_id).cloned();
3185 }
3186 None
3187 }
3188
3189 /// Get the number of parameters a callback function expects by inspecting its AST.
3190 /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
3191 /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
3192 fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
3193 match func_node {
3194 AstNode::Lambda { params, .. } => params.len(),
3195 AstNode::Variable(var_name) => {
3196 // Check if this variable holds a stored lambda
3197 if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
3198 return stored_lambda.params.len();
3199 }
3200 // Also check if it's a lambda value in bindings (e.g., from partial application)
3201 if let Some(value) = self.context.lookup(var_name) {
3202 if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
3203 return stored_lambda.params.len();
3204 }
3205 }
3206 // Unknown, return max to be safe
3207 usize::MAX
3208 }
3209 AstNode::Function { .. } => {
3210 // For function references, we can't easily determine param count
3211 // Return max to be safe
3212 usize::MAX
3213 }
3214 _ => usize::MAX,
3215 }
3216 }
3217
3218 /// Specialized sort using pre-extracted keys (Schwartzian transform).
3219 /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
3220 /// avoiding O(N log N) hash lookups during comparisons.
3221 fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
3222 if arr.len() <= 1 {
3223 return;
3224 }
3225
3226 // Phase 1: Extract sort keys -- one IndexMap lookup per element
3227 let keys: Vec<SortKey> = arr
3228 .iter()
3229 .map(|item| match item {
3230 JValue::Object(obj) => match obj.get(&spec.field) {
3231 Some(JValue::Number(n)) => SortKey::Num(*n),
3232 Some(JValue::String(s)) => SortKey::Str(s.clone()),
3233 _ => SortKey::None,
3234 },
3235 #[cfg(feature = "python")]
3236 JValue::LazyPyDict(lazy) => match lazy.get_field(&spec.field) {
3237 Ok(JValue::Number(n)) => SortKey::Num(n),
3238 Ok(JValue::String(s)) => SortKey::Str(s.clone()),
3239 // Err(_) (conversion failure) and any other value fall through to
3240 // SortKey::None (treated as "missing", sorts last). This arm is only
3241 // reachable for elements that survived upstream evaluation of the sort
3242 // array (T2008 gate / a prior get_field on the same data), so a
3243 // conversion error swallowed here cannot silently mis-sort *today* --
3244 // if this specialized path is ever reached before such validation,
3245 // this arm must be revisited to propagate the error instead.
3246 _ => SortKey::None,
3247 },
3248 _ => SortKey::None,
3249 })
3250 .collect();
3251
3252 // Phase 2: Build index permutation sorted by pre-extracted keys
3253 let mut perm: Vec<usize> = (0..arr.len()).collect();
3254 perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
3255
3256 // Phase 3: Apply permutation in-place via cycle-following
3257 let mut placed = vec![false; arr.len()];
3258 for i in 0..arr.len() {
3259 if placed[i] || perm[i] == i {
3260 continue;
3261 }
3262 let mut j = i;
3263 loop {
3264 let target = perm[j];
3265 placed[j] = true;
3266 if target == i {
3267 break;
3268 }
3269 arr.swap(j, target);
3270 j = target;
3271 }
3272 }
3273 }
3274
3275 /// Merge sort implementation using a comparator function.
3276 /// This replaces the O(n²) bubble sort for better performance on large arrays.
3277 /// The comparator returns true if the first element should come AFTER the second.
3278 fn merge_sort_with_comparator(
3279 &mut self,
3280 arr: &mut [JValue],
3281 comparator: &AstNode,
3282 data: &JValue,
3283 ) -> Result<(), EvaluatorError> {
3284 if arr.len() <= 1 {
3285 return Ok(());
3286 }
3287
3288 // Try specialized fast path for simple field comparisons like
3289 // function($l, $r) { $l.price > $r.price }
3290 if let AstNode::Lambda { params, body, .. } = comparator {
3291 if params.len() >= 2 {
3292 if let Some(spec) = try_specialize_sort_comparator(body, ¶ms[0], ¶ms[1]) {
3293 Self::merge_sort_specialized(arr, &spec);
3294 return Ok(());
3295 }
3296 }
3297 }
3298
3299 let mid = arr.len() / 2;
3300
3301 // Sort left half
3302 self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
3303
3304 // Sort right half
3305 self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
3306
3307 // Merge the sorted halves
3308 let mut temp = Vec::with_capacity(arr.len());
3309 let (left, right) = arr.split_at(mid);
3310
3311 let mut i = 0;
3312 let mut j = 0;
3313
3314 // For lambda comparators, use a reusable scope to avoid
3315 // push_scope/pop_scope per comparison (~n log n total comparisons)
3316 if let AstNode::Lambda { params, body, .. } = comparator {
3317 if params.len() >= 2 {
3318 // Pre-clone param names once outside the loop
3319 let param0 = params[0].clone();
3320 let param1 = params[1].clone();
3321 self.context.push_scope();
3322 while i < left.len() && j < right.len() {
3323 // Reuse scope: clear and rebind instead of push/pop
3324 self.context.clear_current_scope();
3325 self.context.bind(param0.clone(), left[i].clone());
3326 self.context.bind(param1.clone(), right[j].clone());
3327
3328 let cmp_result = self.evaluate_internal(body, data)?;
3329
3330 if self.is_truthy(&cmp_result) {
3331 temp.push(right[j].clone());
3332 j += 1;
3333 } else {
3334 temp.push(left[i].clone());
3335 i += 1;
3336 }
3337 }
3338 self.context.pop_scope();
3339 } else {
3340 // Unexpected param count - fall back to generic path
3341 while i < left.len() && j < right.len() {
3342 let cmp_result = self.apply_function(
3343 comparator,
3344 &[left[i].clone(), right[j].clone()],
3345 data,
3346 )?;
3347 if self.is_truthy(&cmp_result) {
3348 temp.push(right[j].clone());
3349 j += 1;
3350 } else {
3351 temp.push(left[i].clone());
3352 i += 1;
3353 }
3354 }
3355 }
3356 } else {
3357 // Non-lambda comparator: use generic apply_function path
3358 while i < left.len() && j < right.len() {
3359 let cmp_result =
3360 self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
3361 if self.is_truthy(&cmp_result) {
3362 temp.push(right[j].clone());
3363 j += 1;
3364 } else {
3365 temp.push(left[i].clone());
3366 i += 1;
3367 }
3368 }
3369 }
3370
3371 // Copy remaining elements
3372 temp.extend_from_slice(&left[i..]);
3373 temp.extend_from_slice(&right[j..]);
3374
3375 // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
3376 for (i, val) in temp.into_iter().enumerate() {
3377 arr[i] = val;
3378 }
3379
3380 Ok(())
3381 }
3382
3383 /// Evaluate an AST node against data
3384 ///
3385 /// This is the main entry point for evaluation. It sets up the parent context
3386 /// to be the root data if not already set.
3387 ///
3388 /// Also the single choke point for stripping any lingering tuple-stream
3389 /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
3390 /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
3391 /// tuple-stream representation (see `create_tuple_stream`), and without this
3392 /// a bare (or object/array-nested) tuple-producing expression would leak
3393 /// that internal representation into user-visible output instead of the
3394 /// plain value.
3395 pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
3396 // Set parent context to root data if not already set
3397 if self.context.get_parent().is_none() {
3398 self.context.set_parent(data.clone());
3399 }
3400
3401 if self.options.timeout_ms.is_some() {
3402 self.start_time = Some(Instant::now());
3403 }
3404
3405 self.tuple_stream_created = false;
3406 let result = self.evaluate_internal(node, data)?;
3407 Ok(if self.tuple_stream_created {
3408 unwrap_tuple_output(result)
3409 } else {
3410 result
3411 })
3412 }
3413
3414 /// Fast evaluation for leaf nodes that don't need recursion tracking.
3415 /// Returns Some for literals, simple field access on objects, and simple variable lookups.
3416 /// Returns None for anything requiring the full evaluator.
3417 #[inline(always)]
3418 fn evaluate_leaf(
3419 &mut self,
3420 node: &AstNode,
3421 data: &JValue,
3422 ) -> Option<Result<JValue, EvaluatorError>> {
3423 match node {
3424 AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
3425 AstNode::Number(n) => {
3426 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3427 Some(Ok(JValue::from(*n as i64)))
3428 } else {
3429 Some(Ok(JValue::Number(*n)))
3430 }
3431 }
3432 AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
3433 AstNode::Null => Some(Ok(JValue::Null)),
3434 AstNode::Undefined => Some(Ok(JValue::Undefined)),
3435 AstNode::Name(field_name) => match data {
3436 // Array mapping and other cases need full evaluator
3437 JValue::Object(obj) => Some(Ok(obj
3438 .get(field_name)
3439 .cloned()
3440 .unwrap_or(JValue::Undefined))),
3441 #[cfg(feature = "python")]
3442 JValue::LazyPyDict(lazy) => {
3443 Some(lazy.get_field(field_name).map_err(EvaluatorError::from))
3444 }
3445 _ => None,
3446 },
3447 AstNode::Variable(name) if !name.is_empty() => {
3448 // Simple variable lookup — only fast-path when no tuple data
3449 if let JValue::Object(obj) = data {
3450 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3451 return None; // Tuple data needs full evaluator
3452 }
3453 }
3454 // May be a lambda/builtin — needs full evaluator if None
3455 self.context.lookup(name).map(|value| Ok(value.clone()))
3456 }
3457 _ => None,
3458 }
3459 }
3460
3461 /// Internal evaluation method
3462 fn evaluate_internal(
3463 &mut self,
3464 node: &AstNode,
3465 data: &JValue,
3466 ) -> Result<JValue, EvaluatorError> {
3467 // Fast path for leaf nodes — skip recursion tracking overhead
3468 if let Some(result) = self.evaluate_leaf(node, data) {
3469 return result;
3470 }
3471
3472 // Check recursion depth to prevent stack overflow. `effective_limit` is
3473 // whichever is tighter: the user's `max_stack_depth` guardrail or the
3474 // hardcoded native-stack-safety ceiling (`max_recursion_depth`, always
3475 // 302, GitHub issue #34). The hardcoded ceiling is an always-on backstop
3476 // regardless of user options — only a user limit BELOW it can produce
3477 // D1011; hitting the hardcoded ceiling itself (no option set, or an
3478 // option set at/above 302) still produces U1001.
3479 self.recursion_depth += 1;
3480 let effective_limit = match self.options.max_stack_depth {
3481 Some(limit) => limit.min(self.max_recursion_depth),
3482 None => self.max_recursion_depth,
3483 };
3484 if self.recursion_depth > effective_limit {
3485 self.recursion_depth -= 1;
3486 return Err(EvaluatorError::EvaluationError(
3487 if effective_limit < self.max_recursion_depth {
3488 "D1011: Stack overflow. Check for non-terminating recursive function. Consider rewriting as tail-recursive".to_string()
3489 } else {
3490 format!(
3491 "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
3492 effective_limit
3493 )
3494 },
3495 ));
3496 }
3497
3498 // Check evaluation timeout (D1012). `start_time` is only set (in
3499 // `evaluate()`) when `options.timeout_ms` is configured, so this is a
3500 // single `is_none()` branch of overhead when no timeout is set.
3501 if let Some(timeout_ms) = self.options.timeout_ms {
3502 if let Some(start) = self.start_time {
3503 if start.elapsed().as_millis() as u64 > timeout_ms {
3504 self.recursion_depth -= 1;
3505 return Err(EvaluatorError::EvaluationError(format!(
3506 "D1012: Evaluation timeout after {} milliseconds. Check for infinite loop",
3507 timeout_ms
3508 )));
3509 }
3510 }
3511 }
3512
3513 // The soft depth counter above is calibrated against a comfortably
3514 // large native stack. Hosts with a much smaller default thread stack
3515 // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
3516 // stack well before this counter trips, crashing the process instead
3517 // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
3518 // transparently swaps in a bigger stack segment when headroom is
3519 // low, so this stays a no-op cost on the common shallow path.
3520 const RED_ZONE: usize = 128 * 1024;
3521 const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
3522 let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
3523 self.evaluate_internal_impl(node, data)
3524 });
3525
3526 self.recursion_depth -= 1;
3527 result
3528 }
3529
3530 /// Internal evaluation implementation (separated to allow depth tracking)
3531 fn evaluate_internal_impl(
3532 &mut self,
3533 node: &AstNode,
3534 data: &JValue,
3535 ) -> Result<JValue, EvaluatorError> {
3536 match node {
3537 AstNode::String(s) => Ok(JValue::string(s.clone())),
3538
3539 // Name nodes represent field access on the current data
3540 AstNode::Name(field_name) => {
3541 match data {
3542 JValue::Object(obj) => {
3543 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3544 }
3545 JValue::Array(arr) => {
3546 // Map over array
3547 let mut result = Vec::new();
3548 for item in arr.iter() {
3549 if let JValue::Object(obj) = item {
3550 if let Some(val) = obj.get(field_name) {
3551 result.push(val.clone());
3552 }
3553 }
3554 }
3555 if result.is_empty() {
3556 Ok(JValue::Undefined)
3557 } else if result.len() == 1 {
3558 Ok(result.into_iter().next().unwrap())
3559 } else {
3560 Ok(JValue::array(result))
3561 }
3562 }
3563 _ => Ok(JValue::Undefined),
3564 }
3565 }
3566
3567 AstNode::Number(n) => {
3568 // Preserve integer-ness: if the number is a whole number, create an integer JValue
3569 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
3570 // It's a whole number that can be represented as i64
3571 Ok(JValue::from(*n as i64))
3572 } else {
3573 Ok(JValue::Number(*n))
3574 }
3575 }
3576 AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3577 AstNode::Null => Ok(JValue::Null),
3578 AstNode::Undefined => Ok(JValue::Undefined),
3579 AstNode::Placeholder => {
3580 // Placeholders should only appear as function arguments
3581 // If we reach here, it's an error
3582 Err(EvaluatorError::EvaluationError(
3583 "Placeholder '?' can only be used as a function argument".to_string(),
3584 ))
3585 }
3586 AstNode::Regex { pattern, flags } => {
3587 // Return a regex object as a special JSON value
3588 // This will be recognized by functions like $split, $match, $replace
3589 Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3590 }
3591
3592 AstNode::Variable(name) => {
3593 // Special case: $ alone (empty name) refers to current context
3594 // First check if $ is bound in the context (for closures that captured $)
3595 // Otherwise, use the data parameter
3596 if name.is_empty() {
3597 if let Some(value) = self.context.lookup("$") {
3598 return Ok(value.clone());
3599 }
3600 // If data is a tuple, return the @ value
3601 if let JValue::Object(obj) = data {
3602 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3603 if let Some(inner) = obj.get("@") {
3604 return Ok(inner.clone());
3605 }
3606 }
3607 }
3608 return Ok(data.clone());
3609 }
3610
3611 // Check variable bindings FIRST
3612 // This allows function parameters to shadow outer lambdas with the same name
3613 // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3614 if let Some(value) = self.context.lookup(name) {
3615 return Ok(value.clone());
3616 }
3617
3618 // Check tuple bindings in data (for index binding operator #$var)
3619 // When iterating over a tuple stream, $var can reference the bound index
3620 if let JValue::Object(obj) = data {
3621 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3622 // Check for the variable in tuple bindings (stored as "$name")
3623 let binding_key = format!("${}", name);
3624 if let Some(binding_value) = obj.get(&binding_key) {
3625 return Ok(binding_value.clone());
3626 }
3627 }
3628 }
3629
3630 // Then check if this is a stored lambda (user-defined functions)
3631 if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3632 // Return a lambda representation that can be passed to higher-order functions
3633 // Include _lambda_id pointing to the stored lambda so it can be found
3634 // when captured in closures
3635 let lambda_repr = JValue::lambda(
3636 name.as_str(),
3637 stored_lambda.params.clone(),
3638 Some(name.to_string()),
3639 stored_lambda.signature.clone(),
3640 );
3641 return Ok(lambda_repr);
3642 }
3643
3644 // Check if this is a built-in function reference (only if not shadowed)
3645 if self.is_builtin_function(name) {
3646 // Return a marker for built-in functions
3647 // This allows built-in functions to be passed to higher-order functions
3648 let builtin_repr = JValue::builtin(name.as_str());
3649 return Ok(builtin_repr);
3650 }
3651
3652 // Undefined variable - return null (undefined in JSONata semantics)
3653 // This allows expressions like `$not(undefined_var)` to return undefined
3654 // and comparisons like `3 > $undefined` to return undefined
3655 Ok(JValue::Null)
3656 }
3657
3658 AstNode::ParentVariable(name) => {
3659 // Special case: $$ alone (empty name) refers to parent/root context
3660 if name.is_empty() {
3661 return self.context.get_parent().cloned().ok_or_else(|| {
3662 EvaluatorError::ReferenceError("Parent context not available".to_string())
3663 });
3664 }
3665
3666 // For $$name, we need to evaluate name against parent context
3667 // This is similar to $.name but using parent data
3668 let parent_data = self.context.get_parent().ok_or_else(|| {
3669 EvaluatorError::ReferenceError("Parent context not available".to_string())
3670 })?;
3671
3672 // Access field on parent context
3673 match parent_data {
3674 JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3675 _ => Ok(JValue::Null),
3676 }
3677 }
3678
3679 AstNode::Path { steps } => self.evaluate_path(steps, data),
3680
3681 AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3682
3683 AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3684
3685 // Array constructor - JSONata semantics:
3686 AstNode::Array(elements) => {
3687 // - If element is itself an array constructor [...], keep it nested
3688 // - Otherwise, if element evaluates to an array, flatten it
3689 // - Undefined values are excluded
3690 let mut result = Vec::with_capacity(elements.len());
3691 for element in elements {
3692 // Check if this element is itself an explicit array constructor
3693 let is_array_constructor = matches!(element, AstNode::Array(_));
3694
3695 let value = self.evaluate_internal(element, data)?;
3696
3697 // Skip undefined values in array constructors
3698 // Note: explicit null is preserved, only undefined (no value) is filtered
3699 if value.is_undefined() {
3700 continue;
3701 }
3702
3703 if is_array_constructor {
3704 // Explicit array constructor - keep nested
3705 result.push(value);
3706 } else if let JValue::Array(arr) = value {
3707 // Non-array-constructor that evaluated to array - flatten it
3708 result.extend(arr.iter().cloned());
3709 } else {
3710 // Non-array value - add as-is
3711 result.push(value);
3712 }
3713 }
3714 Ok(JValue::array(result))
3715 }
3716
3717 AstNode::Object(pairs) => {
3718 let mut result = IndexMap::with_capacity(pairs.len());
3719
3720 // Check if all keys are string literals — can skip D1009 HashMap
3721 let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3722
3723 if all_literal_keys {
3724 // Fast path: literal keys, no need for D1009 tracking
3725 for (key_node, value_node) in pairs.iter() {
3726 let key = match key_node {
3727 AstNode::String(s) => s,
3728 _ => unreachable!(),
3729 };
3730 let value = self.evaluate_internal(value_node, data)?;
3731 if value.is_undefined() {
3732 continue;
3733 }
3734 result.insert(key.clone(), value);
3735 }
3736 } else {
3737 let mut key_sources: HashMap<String, usize> = HashMap::new();
3738 for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3739 let key = match self.evaluate_internal(key_node, data)? {
3740 JValue::String(s) => s,
3741 JValue::Null => continue,
3742 other => {
3743 if other.is_undefined() {
3744 continue;
3745 }
3746 return Err(EvaluatorError::TypeError(format!(
3747 "Object key must be a string, got: {:?}",
3748 other
3749 )));
3750 }
3751 };
3752
3753 if let Some(&existing_idx) = key_sources.get(&*key) {
3754 if existing_idx != pair_index {
3755 return Err(EvaluatorError::EvaluationError(format!(
3756 "D1009: Multiple key expressions evaluate to same key: {}",
3757 key
3758 )));
3759 }
3760 }
3761 key_sources.insert(key.to_string(), pair_index);
3762
3763 let value = self.evaluate_internal(value_node, data)?;
3764 if value.is_undefined() {
3765 continue;
3766 }
3767 result.insert(key.to_string(), value);
3768 }
3769 }
3770 Ok(JValue::object(result))
3771 }
3772
3773 // Object transform: group items by key, then evaluate value once per group
3774 AstNode::ObjectTransform { input, pattern } => {
3775 // Evaluate the input expression. Keep tuple wrappers alive so the
3776 // group-by key/value expressions can read the carried `$focus`
3777 // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3778 let saved_keep = self.keep_tuple_stream;
3779 self.keep_tuple_stream = true;
3780 let input_value = self.evaluate_internal(input, data);
3781 self.keep_tuple_stream = saved_keep;
3782 let input_value = input_value?;
3783
3784 // If input is undefined, return undefined (not empty object)
3785 if input_value.is_undefined() {
3786 return Ok(JValue::Undefined);
3787 }
3788
3789 // Handle array input - process each item
3790 let items: Vec<JValue> = match input_value {
3791 JValue::Array(ref arr) => (**arr).clone(),
3792 JValue::Null => return Ok(JValue::Null),
3793 other => vec![other],
3794 };
3795
3796 // If array is empty, add undefined to enable literal JSON object generation
3797 let items = if items.is_empty() {
3798 vec![JValue::Undefined]
3799 } else {
3800 items
3801 };
3802
3803 // Grouping over a tuple stream ("reduce" mode, mirroring
3804 // jsonata-js evaluateGroupExpression): each item is a
3805 // `{@, $var, !label, __tuple__}` wrapper. The key/value
3806 // expressions are evaluated against the tuple's `@` value with the
3807 // carried focus/index/ancestor keys bound into scope (so
3808 // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3809 // `$e` AND resolves the relative `Phone` against the Contact `@`),
3810 // and grouped tuples are reduced (per-key values appended) before
3811 // the value expression sees them.
3812 let reduce = items.first().is_some_and(|it| {
3813 matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3814 });
3815
3816 // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3817 // returns the saved prior values so they can be restored.
3818 let bind_tuple = |ev: &mut Self,
3819 tuple: &IndexMap<String, JValue>|
3820 -> Vec<(String, Option<JValue>)> {
3821 let mut saved = Vec::new();
3822 for (k, v) in tuple.iter() {
3823 let name = if let Some(n) = k.strip_prefix('$') {
3824 if n.is_empty() {
3825 continue;
3826 } else {
3827 n.to_string()
3828 }
3829 } else if k.starts_with('!') {
3830 k.clone()
3831 } else {
3832 continue;
3833 };
3834 saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3835 ev.context.bind(name, v.clone());
3836 }
3837 saved
3838 };
3839 let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3840 for (name, old) in saved.into_iter().rev() {
3841 match old {
3842 Some(v) => ev.context.bind(name, v),
3843 None => ev.context.unbind(&name),
3844 }
3845 }
3846 };
3847
3848 // Phase 1: Group items by key expression
3849 // groups maps key -> (grouped_data, expr_index)
3850 // When multiple items have same key, their data is appended together
3851 let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3852
3853 // Save the current $ binding to restore later
3854 let saved_dollar = self.context.lookup("$").cloned();
3855
3856 for item in &items {
3857 // In reduce mode evaluate the key against `@` with tuple keys
3858 // bound; otherwise against the item itself.
3859 let (key_data, tuple_saved) = match (reduce, item) {
3860 (true, JValue::Object(o)) => {
3861 let saved = bind_tuple(self, o);
3862 (
3863 o.get("@").cloned().unwrap_or(JValue::Undefined),
3864 Some(saved),
3865 )
3866 }
3867 _ => (item.clone(), None),
3868 };
3869 self.context.bind("$".to_string(), key_data.clone());
3870
3871 for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3872 // Evaluate key with current item as context
3873 let key = match self.evaluate_internal(key_node, &key_data)? {
3874 JValue::String(s) => s,
3875 JValue::Null => continue, // Skip null keys
3876 other => {
3877 // Skip undefined keys
3878 if other.is_undefined() {
3879 continue;
3880 }
3881 if let Some(saved) = tuple_saved {
3882 restore(self, saved);
3883 }
3884 return Err(EvaluatorError::TypeError(format!(
3885 "T1003: Object key must be a string, got: {:?}",
3886 other
3887 )));
3888 }
3889 };
3890
3891 // Group items by key
3892 if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3893 // Key already exists - check if from same expression index
3894 if *existing_idx != pair_index {
3895 if let Some(saved) = tuple_saved {
3896 restore(self, saved);
3897 }
3898 // D1009: multiple key expressions evaluate to same key
3899 return Err(EvaluatorError::EvaluationError(format!(
3900 "D1009: Multiple key expressions evaluate to same key: {}",
3901 key
3902 )));
3903 }
3904 // Append item to the group
3905 existing_data.push(item.clone());
3906 } else {
3907 // New key - create new group
3908 groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3909 }
3910 }
3911
3912 if let Some(saved) = tuple_saved {
3913 restore(self, saved);
3914 }
3915 }
3916
3917 // Phase 2: Evaluate value expression for each group
3918 let mut result = IndexMap::new();
3919
3920 for (key, (grouped_data, expr_index)) in groups {
3921 // Get the value expression for this group
3922 let (_key_node, value_node) = &pattern[expr_index];
3923
3924 if reduce {
3925 // Reduce the grouped tuples into one (per-key values
3926 // appended), mirroring jsonata-js reduceTupleStream, then
3927 // evaluate the value against the merged `@` with the merged
3928 // focus/index/ancestor keys bound.
3929 let merged = reduce_tuple_stream(&grouped_data);
3930 let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3931 let mut tuple_no_at = merged.clone();
3932 tuple_no_at.shift_remove("@");
3933 let saved = bind_tuple(self, &tuple_no_at);
3934 self.context.bind("$".to_string(), context.clone());
3935 let value = self.evaluate_internal(value_node, &context);
3936 restore(self, saved);
3937 let value = value?;
3938 if !value.is_undefined() {
3939 result.insert(key, value);
3940 }
3941 continue;
3942 }
3943
3944 // Determine the context for value evaluation:
3945 // - If single item, use that item directly
3946 // - If multiple items, use the array of items
3947 let context = if grouped_data.len() == 1 {
3948 grouped_data.into_iter().next().unwrap()
3949 } else {
3950 JValue::array(grouped_data)
3951 };
3952
3953 // Bind $ to the context for value evaluation
3954 self.context.bind("$".to_string(), context.clone());
3955
3956 // Evaluate value expression with grouped context
3957 let value = self.evaluate_internal(value_node, &context)?;
3958
3959 // Skip undefined values
3960 if !value.is_undefined() {
3961 result.insert(key, value);
3962 }
3963 }
3964
3965 // Restore the previous $ binding
3966 if let Some(saved) = saved_dollar {
3967 self.context.bind("$".to_string(), saved);
3968 } else {
3969 self.context.unbind("$");
3970 }
3971
3972 Ok(JValue::object(result))
3973 }
3974
3975 AstNode::Function {
3976 name,
3977 args,
3978 is_builtin,
3979 } => self.evaluate_function_call(name, args, *is_builtin, data),
3980
3981 // Call: invoke an arbitrary expression as a function
3982 // Used for IIFE patterns like (function($x){...})(5) or chained calls
3983 AstNode::Call { procedure, args } => {
3984 // Evaluate the procedure to get the callable value
3985 let callable = self.evaluate_internal(procedure, data)?;
3986
3987 // Check if it's a lambda value
3988 if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3989 let mut evaluated_args = Vec::with_capacity(args.len());
3990 for arg in args.iter() {
3991 evaluated_args.push(self.evaluate_internal(arg, data)?);
3992 }
3993 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3994 }
3995
3996 // Not a callable value
3997 Err(EvaluatorError::TypeError(format!(
3998 "Cannot call non-function value: {:?}",
3999 callable
4000 )))
4001 }
4002
4003 AstNode::Conditional {
4004 condition,
4005 then_branch,
4006 else_branch,
4007 } => {
4008 let condition_value = self.evaluate_internal(condition, data)?;
4009 if self.is_truthy(&condition_value) {
4010 self.evaluate_internal(then_branch, data)
4011 } else if let Some(else_branch) = else_branch {
4012 self.evaluate_internal(else_branch, data)
4013 } else {
4014 // No else branch - return undefined (not null)
4015 // This allows $map to filter out results from conditionals without else
4016 Ok(JValue::Undefined)
4017 }
4018 }
4019
4020 AstNode::Block(expressions) => {
4021 // Blocks create a new scope - push scope instead of clone/restore
4022 self.context.push_scope();
4023
4024 let mut result = JValue::Null;
4025 for expr in expressions {
4026 result = self.evaluate_internal(expr, data)?;
4027 }
4028
4029 // Before popping, preserve any lambdas referenced by the result
4030 // This is essential for closures returned from blocks (IIFE pattern)
4031 let lambdas_to_keep = self.extract_lambda_ids(&result);
4032 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
4033
4034 Ok(result)
4035 }
4036
4037 // Lambda: capture current environment for closure support
4038 AstNode::Lambda {
4039 params,
4040 body,
4041 signature,
4042 thunk,
4043 } => {
4044 let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
4045
4046 let compiled_body = if !thunk {
4047 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
4048 try_compile_expr_with_allowed_vars(body, &var_refs)
4049 } else {
4050 None
4051 };
4052 let stored_lambda = StoredLambda {
4053 params: params.clone(),
4054 body: (**body).clone(),
4055 compiled_body,
4056 signature: signature.clone(),
4057 captured_env: self.capture_environment_for(body, params),
4058 captured_data: Some(data.clone()),
4059 thunk: *thunk,
4060 };
4061 self.context.bind_lambda(lambda_id.clone(), stored_lambda);
4062
4063 let lambda_obj = JValue::lambda(
4064 lambda_id.as_str(),
4065 params.clone(),
4066 None::<String>,
4067 signature.clone(),
4068 );
4069
4070 Ok(lambda_obj)
4071 }
4072
4073 // Wildcard: collect all values from current object
4074 AstNode::Wildcard => {
4075 let normalized = normalize_lazy(data)?;
4076 match &normalized {
4077 JValue::Object(obj) => {
4078 let mut result = Vec::new();
4079 for value in obj.values() {
4080 // Flatten arrays into the result
4081 match value {
4082 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4083 _ => result.push(value.clone()),
4084 }
4085 }
4086 check_sequence_length(result.len(), &self.options)?;
4087 Ok(JValue::array(result))
4088 }
4089 JValue::Array(arr) => {
4090 // For arrays, wildcard returns all elements
4091 Ok(JValue::Array(arr.clone()))
4092 }
4093 _ => Ok(JValue::Null),
4094 }
4095 }
4096
4097 // Descendant: recursively traverse all nested values
4098 AstNode::Descendant => {
4099 let descendants = self.collect_descendants(data)?;
4100 if descendants.is_empty() {
4101 Ok(JValue::Null) // No descendants means undefined
4102 } else {
4103 check_sequence_length(descendants.len(), &self.options)?;
4104 Ok(JValue::array(descendants))
4105 }
4106 }
4107
4108 AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
4109 "Predicate can only be used in path expressions".to_string(),
4110 )),
4111
4112 // Array grouping: same as Array but prevents flattening in path contexts
4113 AstNode::ArrayGroup(elements) => {
4114 let mut result = Vec::new();
4115 for element in elements {
4116 let value = self.evaluate_internal(element, data)?;
4117 result.push(value);
4118 }
4119 Ok(JValue::array(result))
4120 }
4121
4122 AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
4123 "Function application can only be used in path expressions".to_string(),
4124 )),
4125
4126 AstNode::Sort { input, terms } => {
4127 // Keep the input path's tuple wrappers so the sort terms can read
4128 // the carried `%`/`$focus`/`$index` bindings per element.
4129 let saved = self.keep_tuple_stream;
4130 self.keep_tuple_stream = true;
4131 let value = self.evaluate_internal(input, data);
4132 self.keep_tuple_stream = saved;
4133 self.evaluate_sort(&value?, terms)
4134 }
4135
4136 // Transform: |location|update[,delete]|
4137 AstNode::Transform {
4138 location,
4139 update,
4140 delete,
4141 } => {
4142 // Check if $ is bound (meaning we're being invoked as a lambda)
4143 if self.context.lookup("$").is_some() {
4144 // Execute the transformation
4145 self.execute_transform(location, update, delete.as_deref(), data)
4146 } else {
4147 // Return a lambda representation
4148 // The transform will be executed when the lambda is invoked
4149 let transform_lambda = StoredLambda {
4150 params: vec!["$".to_string()],
4151 body: AstNode::Transform {
4152 location: location.clone(),
4153 update: update.clone(),
4154 delete: delete.clone(),
4155 },
4156 compiled_body: None, // Transform is not a pure compilable expr
4157 signature: None,
4158 captured_env: HashMap::new(),
4159 captured_data: None, // Transform takes $ as parameter
4160 thunk: false,
4161 };
4162
4163 // Store with a generated unique name
4164 let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
4165 self.context.bind_lambda(lambda_name, transform_lambda);
4166
4167 // Return lambda marker
4168 Ok(JValue::string("<lambda>"))
4169 }
4170 }
4171
4172 // Parent-reference operator (%): ast_transform has already resolved
4173 // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
4174 // tuple step binds that label into scope (create_tuple_stream +
4175 // needs_tuple_context_binding), so resolving it is an ordinary scope
4176 // lookup, mirroring jsonata-js's
4177 // `case 'parent': result = environment.lookup(expr.slot.label);`.
4178 AstNode::Parent(label) => {
4179 if let Some(v) = self.context.lookup(label) {
4180 return Ok(v.clone());
4181 }
4182 // Fall back to the tuple wrapper carried as `data`: a `%` used
4183 // inside a predicate/stage over a tuple stream -- e.g.
4184 // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
4185 // predicate is evaluated per tuple with the wrapper as data --
4186 // reads its ancestor from the tuple's `!label` key, which isn't
4187 // separately bound into scope here (mirrors AstNode::Variable's
4188 // tuple-binding fallback below).
4189 if let JValue::Object(obj) = data {
4190 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4191 if let Some(v) = obj.get(label) {
4192 return Ok(v.clone());
4193 }
4194 }
4195 }
4196 Ok(JValue::Undefined)
4197 }
4198 }
4199 }
4200
4201 /// Apply stages (filters/predicates) to a value during field extraction
4202 /// Non-array values are wrapped in an array before filtering (JSONata semantics)
4203 /// This matches the JavaScript reference where stages apply to sequences
4204 fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
4205 // Wrap non-arrays in an array for filtering (JSONata semantics)
4206 let mut result = match value {
4207 JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
4208 JValue::Array(_) => value,
4209 other => JValue::array(vec![other]),
4210 };
4211
4212 for stage in stages {
4213 match stage {
4214 Stage::Filter(predicate_expr) => {
4215 // When applying stages, use stage-specific predicate logic
4216 result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
4217 }
4218 // Positional index stages are meaningful only over a tuple stream
4219 // (they set a variable to each tuple's position); they are applied
4220 // in `create_tuple_stream`, not on a plain value sequence here.
4221 Stage::Index(_) => {}
4222 }
4223 }
4224 Ok(result)
4225 }
4226
4227 /// Check if an AST node is definitely a filter expression (comparison/logical)
4228 /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
4229 fn is_filter_predicate(predicate: &AstNode) -> bool {
4230 match predicate {
4231 AstNode::Binary { op, .. } => matches!(
4232 op,
4233 BinaryOp::GreaterThan
4234 | BinaryOp::GreaterThanOrEqual
4235 | BinaryOp::LessThan
4236 | BinaryOp::LessThanOrEqual
4237 | BinaryOp::Equal
4238 | BinaryOp::NotEqual
4239 | BinaryOp::And
4240 | BinaryOp::Or
4241 | BinaryOp::In
4242 ),
4243 AstNode::Unary {
4244 op: crate::ast::UnaryOp::Not,
4245 ..
4246 } => true,
4247 _ => false,
4248 }
4249 }
4250
4251 /// Evaluate a predicate as a stage during field extraction
4252 /// This has different semantics than standalone predicates:
4253 /// - Maps index operations over arrays of extracted values
4254 fn evaluate_predicate_as_stage(
4255 &mut self,
4256 current: &JValue,
4257 predicate: &AstNode,
4258 ) -> Result<JValue, EvaluatorError> {
4259 // Special case: empty brackets [] (represented as Boolean(true))
4260 if matches!(predicate, AstNode::Boolean(true)) {
4261 return match current {
4262 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
4263 JValue::Null => Ok(JValue::Null),
4264 other => Ok(JValue::array(vec![other.clone()])),
4265 };
4266 }
4267
4268 match current {
4269 JValue::Array(arr) => {
4270 // For stages: if we have an array of values (from field extraction),
4271 // apply the predicate to each value if appropriate
4272
4273 // Check if predicate is a numeric index
4274 if let AstNode::Number(n) = predicate {
4275 // Check if this is an array of arrays (extracted array fields)
4276 let is_array_of_arrays =
4277 arr.iter().any(|item| matches!(item, JValue::Array(_)));
4278
4279 if !is_array_of_arrays {
4280 // Simple values: just index normally
4281 return self.array_index(current, &JValue::Number(*n));
4282 }
4283
4284 // Array of arrays: map index access over each extracted array
4285 let mut result = Vec::new();
4286 for item in arr.iter() {
4287 match item {
4288 JValue::Array(_) => {
4289 let indexed = self.array_index(item, &JValue::Number(*n))?;
4290 if !indexed.is_null() && !indexed.is_undefined() {
4291 result.push(indexed);
4292 }
4293 }
4294 _ => {
4295 if *n == 0.0 {
4296 result.push(item.clone());
4297 }
4298 }
4299 }
4300 }
4301 return Ok(JValue::array(result));
4302 }
4303
4304 // Short-circuit: if predicate is definitely a comparison/logical expression,
4305 // skip speculative numeric evaluation and go directly to filter logic
4306 if Self::is_filter_predicate(predicate) {
4307 // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
4308 if let Some(compiled) = try_compile_expr(predicate) {
4309 let shape = arr.first().and_then(build_shape_cache);
4310 let mut filtered = Vec::with_capacity(arr.len());
4311 for item in arr.iter() {
4312 let result = if let Some(ref s) = shape {
4313 eval_compiled_shaped(
4314 &compiled,
4315 item,
4316 None,
4317 s,
4318 &self.options,
4319 self.start_time,
4320 )?
4321 } else {
4322 eval_compiled(
4323 &compiled,
4324 item,
4325 None,
4326 &self.options,
4327 self.start_time,
4328 )?
4329 };
4330 if compiled_is_truthy(&result) {
4331 filtered.push(item.clone());
4332 }
4333 }
4334 return Ok(JValue::array(filtered));
4335 }
4336 // Fallback: full AST evaluation
4337 let mut filtered = Vec::new();
4338 for item in arr.iter() {
4339 let item_result = self.evaluate_internal(predicate, item)?;
4340 if self.is_truthy(&item_result) {
4341 filtered.push(item.clone());
4342 }
4343 }
4344 return Ok(JValue::array(filtered));
4345 }
4346
4347 // Try to evaluate the predicate to see if it's a numeric index or array of indices
4348 // If evaluation succeeds and yields a number, use it as an index
4349 // If it yields an array of numbers, use them as multiple indices
4350 // If evaluation fails (e.g., comparison error), treat as filter
4351 match self.evaluate_internal(predicate, current) {
4352 Ok(JValue::Number(n)) => {
4353 let n_val = n;
4354 let is_array_of_arrays =
4355 arr.iter().any(|item| matches!(item, JValue::Array(_)));
4356
4357 if !is_array_of_arrays {
4358 let pred_result = JValue::Number(n_val);
4359 return self.array_index(current, &pred_result);
4360 }
4361
4362 // Array of arrays: map index access
4363 let mut result = Vec::new();
4364 let pred_result = JValue::Number(n_val);
4365 for item in arr.iter() {
4366 match item {
4367 JValue::Array(_) => {
4368 let indexed = self.array_index(item, &pred_result)?;
4369 if !indexed.is_null() && !indexed.is_undefined() {
4370 result.push(indexed);
4371 }
4372 }
4373 _ => {
4374 if n_val == 0.0 {
4375 result.push(item.clone());
4376 }
4377 }
4378 }
4379 }
4380 return Ok(JValue::array(result));
4381 }
4382 Ok(JValue::Array(indices)) => {
4383 // Array of values - could be indices or filter results
4384 // Check if all values are numeric
4385 let has_non_numeric =
4386 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
4387
4388 if has_non_numeric {
4389 // Non-numeric values - treat as filter, fall through
4390 } else {
4391 // All numeric - use as indices
4392 let arr_len = arr.len() as i64;
4393 let mut resolved_indices: Vec<i64> = indices
4394 .iter()
4395 .filter_map(|v| {
4396 if let JValue::Number(n) = v {
4397 let idx = *n as i64;
4398 // Resolve negative indices
4399 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
4400 // Only include valid indices
4401 if actual_idx >= 0 && actual_idx < arr_len {
4402 Some(actual_idx)
4403 } else {
4404 None
4405 }
4406 } else {
4407 None
4408 }
4409 })
4410 .collect();
4411
4412 // Sort and deduplicate indices
4413 resolved_indices.sort();
4414 resolved_indices.dedup();
4415
4416 // Select elements at each sorted index
4417 let result: Vec<JValue> = resolved_indices
4418 .iter()
4419 .map(|&idx| arr[idx as usize].clone())
4420 .collect();
4421
4422 return Ok(JValue::array(result));
4423 }
4424 }
4425 Ok(_) => {
4426 // Evaluated successfully but not a number or array - might be a filter
4427 // Fall through to filter logic
4428 }
4429 Err(_) => {
4430 // Evaluation failed - it's likely a filter expression
4431 // Fall through to filter logic
4432 }
4433 }
4434
4435 // It's a filter expression
4436 let mut filtered = Vec::new();
4437 for item in arr.iter() {
4438 let item_result = self.evaluate_internal(predicate, item)?;
4439 if self.is_truthy(&item_result) {
4440 filtered.push(item.clone());
4441 }
4442 }
4443 Ok(JValue::array(filtered))
4444 }
4445 JValue::Null => {
4446 // Null: return null
4447 Ok(JValue::Null)
4448 }
4449 other => {
4450 // Non-array values: treat as single-element conceptual array
4451 // For numeric predicates: index 0 returns the value, other indices return null
4452 // For boolean predicates: if truthy, return value; if falsy, return null
4453
4454 // Check if predicate is a numeric index
4455 if let AstNode::Number(n) = predicate {
4456 // Index 0 returns the value, other indices return null
4457 if *n == 0.0 {
4458 return Ok(other.clone());
4459 } else {
4460 return Ok(JValue::Null);
4461 }
4462 }
4463
4464 // Try to evaluate the predicate to see if it's a numeric index
4465 match self.evaluate_internal(predicate, other) {
4466 Ok(JValue::Number(n)) => {
4467 // Index 0 returns the value, other indices return null
4468 if n == 0.0 {
4469 Ok(other.clone())
4470 } else {
4471 Ok(JValue::Null)
4472 }
4473 }
4474 Ok(pred_result) => {
4475 // Boolean filter: return value if truthy, null if falsy
4476 if self.is_truthy(&pred_result) {
4477 Ok(other.clone())
4478 } else {
4479 Ok(JValue::Null)
4480 }
4481 }
4482 Err(e) => Err(e),
4483 }
4484 }
4485 }
4486 }
4487
4488 /// Evaluate a path expression (e.g., foo.bar.baz)
4489 fn evaluate_path(
4490 &mut self,
4491 steps: &[PathStep],
4492 data: &JValue,
4493 ) -> Result<JValue, EvaluatorError> {
4494 // Avoid cloning by using references and only cloning when necessary
4495 if steps.is_empty() {
4496 return Ok(data.clone());
4497 }
4498
4499 // Fast path: single field access on object
4500 // This is a very common pattern, so optimize it.
4501 // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
4502 // creation handled below.
4503 if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
4504 if let AstNode::Name(field_name) = &steps[0].node {
4505 return match data {
4506 JValue::Object(obj) => {
4507 // Check if this is a tuple - extract '@' value
4508 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
4509 match obj.get("@") {
4510 Some(JValue::Object(inner)) => {
4511 Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
4512 }
4513 #[cfg(feature = "python")]
4514 Some(JValue::LazyPyDict(lazy)) => Ok(lazy.get_field(field_name)?),
4515 _ => Ok(JValue::Undefined),
4516 }
4517 } else {
4518 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
4519 }
4520 }
4521 #[cfg(feature = "python")]
4522 JValue::LazyPyDict(lazy) => Ok(lazy.get_field(field_name)?),
4523 JValue::Array(arr) => {
4524 // Array mapping: extract field from each element
4525 // Optimized: use references to access fields without cloning entire objects
4526 // Check first element for tuple-ness (tuples are all-or-nothing)
4527 let has_tuples = arr.first().is_some_and(|item| {
4528 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4529 });
4530
4531 if !has_tuples {
4532 // Fast path: no tuples, just direct field lookups
4533 let mut result = Vec::with_capacity(arr.len());
4534 for item in arr.iter() {
4535 if let JValue::Object(obj) = item {
4536 if let Some(val) = obj.get(field_name) {
4537 if !val.is_null() {
4538 match val {
4539 JValue::Array(arr_val) => {
4540 result.extend(arr_val.iter().cloned());
4541 }
4542 other => result.push(other.clone()),
4543 }
4544 }
4545 }
4546 } else if let JValue::Array(inner_arr) = item {
4547 let nested_result = self.evaluate_path(
4548 &[PathStep::new(AstNode::Name(field_name.clone()))],
4549 &JValue::Array(inner_arr.clone()),
4550 )?;
4551 match nested_result {
4552 JValue::Array(nested) => {
4553 result.extend(nested.iter().cloned());
4554 }
4555 JValue::Null => {}
4556 other => result.push(other),
4557 }
4558 } else {
4559 #[cfg(feature = "python")]
4560 if let JValue::LazyPyDict(lazy) = item {
4561 let val = lazy.get_field(field_name)?;
4562 if !val.is_null() && !val.is_undefined() {
4563 match val {
4564 JValue::Array(arr_val) => {
4565 result.extend(arr_val.iter().cloned());
4566 }
4567 other => result.push(other),
4568 }
4569 }
4570 }
4571 }
4572 }
4573
4574 if result.is_empty() {
4575 Ok(JValue::Null)
4576 } else if result.len() == 1 {
4577 Ok(result.into_iter().next().unwrap())
4578 } else {
4579 check_sequence_length(result.len(), &self.options)?;
4580 Ok(JValue::array(result))
4581 }
4582 } else {
4583 // Tuple path: per-element tuple handling
4584 let mut result = Vec::new();
4585 for item in arr.iter() {
4586 match item {
4587 JValue::Object(obj) => {
4588 let is_tuple =
4589 obj.get("__tuple__") == Some(&JValue::Bool(true));
4590
4591 if is_tuple {
4592 let field_val: Option<JValue> = match obj.get("@") {
4593 Some(JValue::Object(inner)) => {
4594 inner.get(field_name).cloned()
4595 }
4596 #[cfg(feature = "python")]
4597 Some(JValue::LazyPyDict(lazy)) => {
4598 let v = lazy.get_field(field_name)?;
4599 if v.is_undefined() {
4600 None
4601 } else {
4602 Some(v)
4603 }
4604 }
4605 _ => continue,
4606 };
4607
4608 if let Some(val) = field_val.as_ref() {
4609 if !val.is_null() {
4610 // Build tuple wrapper - only clone bindings when needed
4611 let wrap = |v: JValue| -> JValue {
4612 let mut wrapper = IndexMap::new();
4613 wrapper.insert("@".to_string(), v);
4614 wrapper.insert(
4615 "__tuple__".to_string(),
4616 JValue::Bool(true),
4617 );
4618 for (k, v) in obj.iter() {
4619 if k.starts_with('$') {
4620 wrapper
4621 .insert(k.clone(), v.clone());
4622 }
4623 }
4624 JValue::object(wrapper)
4625 };
4626
4627 match val {
4628 JValue::Array(arr_val) => {
4629 for item in arr_val.iter() {
4630 result.push(wrap(item.clone()));
4631 }
4632 }
4633 other => result.push(wrap(other.clone())),
4634 }
4635 }
4636 }
4637 } else {
4638 // Non-tuple: access field directly by reference, only clone the field value
4639 if let Some(val) = obj.get(field_name) {
4640 if !val.is_null() {
4641 match val {
4642 JValue::Array(arr_val) => {
4643 for item in arr_val.iter() {
4644 result.push(item.clone());
4645 }
4646 }
4647 other => result.push(other.clone()),
4648 }
4649 }
4650 }
4651 }
4652 }
4653 JValue::Array(inner_arr) => {
4654 // Recursively map over nested array
4655 let nested_result = self.evaluate_path(
4656 &[PathStep::new(AstNode::Name(field_name.clone()))],
4657 &JValue::Array(inner_arr.clone()),
4658 )?;
4659 // Add nested result to our results
4660 match nested_result {
4661 JValue::Array(nested) => {
4662 // Flatten nested arrays from recursive mapping
4663 result.extend(nested.iter().cloned());
4664 }
4665 JValue::Null => {} // Skip nulls from nested arrays
4666 other => result.push(other),
4667 }
4668 }
4669 _ => {} // Skip non-object items
4670 }
4671 }
4672
4673 // Return array result
4674 // JSONata singleton unwrapping: if we have exactly one result,
4675 // unwrap it (even if it's an array)
4676 if result.is_empty() {
4677 Ok(JValue::Null)
4678 } else if result.len() == 1 {
4679 Ok(result.into_iter().next().unwrap())
4680 } else {
4681 check_sequence_length(result.len(), &self.options)?;
4682 Ok(JValue::array(result))
4683 }
4684 } // end else (tuple path)
4685 }
4686 _ => Ok(JValue::Undefined),
4687 };
4688 }
4689 }
4690
4691 // Fast path: 2-step $variable.field with no stages
4692 // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4693 if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4694 if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4695 (&steps[0].node, &steps[1].node)
4696 {
4697 if !var_name.is_empty() {
4698 if let Some(value) = self.context.lookup(var_name) {
4699 match value {
4700 JValue::Object(obj) => {
4701 return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4702 }
4703 #[cfg(feature = "python")]
4704 JValue::LazyPyDict(lazy) => {
4705 let v = lazy.get_field(field_name)?;
4706 return Ok(if v.is_undefined() { JValue::Null } else { v });
4707 }
4708 JValue::Array(arr) => {
4709 // Map field extraction over array (same as single-step Name on Array)
4710 let mut result = Vec::with_capacity(arr.len());
4711 for item in arr.iter() {
4712 if let JValue::Object(obj) = item {
4713 if let Some(val) = obj.get(field_name) {
4714 if !val.is_null() {
4715 match val {
4716 JValue::Array(inner) => {
4717 result.extend(inner.iter().cloned());
4718 }
4719 other => result.push(other.clone()),
4720 }
4721 }
4722 }
4723 } else {
4724 #[cfg(feature = "python")]
4725 if let JValue::LazyPyDict(lazy) = item {
4726 let val = lazy.get_field(field_name)?;
4727 if !val.is_null() && !val.is_undefined() {
4728 match val {
4729 JValue::Array(inner) => {
4730 result.extend(inner.iter().cloned());
4731 }
4732 other => result.push(other),
4733 }
4734 }
4735 }
4736 }
4737 }
4738 return match result.len() {
4739 0 => Ok(JValue::Null),
4740 1 => Ok(result.pop().unwrap()),
4741 _ => {
4742 check_sequence_length(result.len(), &self.options)?;
4743 Ok(JValue::array(result))
4744 }
4745 };
4746 }
4747 _ => {} // Fall through to general path evaluation
4748 }
4749 }
4750 }
4751 }
4752 }
4753
4754 // Track whether we did array mapping (for singleton unwrapping)
4755 let mut did_array_mapping = false;
4756
4757 // For the first step, work with a reference.
4758 // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4759 // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4760 // first path step where tupleBindings is undefined.
4761 let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4762 JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4763 } else {
4764 match &steps[0].node {
4765 AstNode::Wildcard => {
4766 // Wildcard as first step
4767 let normalized = normalize_lazy(data)?;
4768 match &normalized {
4769 JValue::Object(obj) => {
4770 let mut result = Vec::new();
4771 for value in obj.values() {
4772 // Flatten arrays into the result
4773 match value {
4774 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4775 _ => result.push(value.clone()),
4776 }
4777 }
4778 JValue::array(result)
4779 }
4780 JValue::Array(arr) => JValue::Array(arr.clone()),
4781 _ => JValue::Null,
4782 }
4783 }
4784 AstNode::Descendant => {
4785 // Descendant as first step
4786 let descendants = self.collect_descendants(data)?;
4787 JValue::array(descendants)
4788 }
4789 AstNode::ParentVariable(name) => {
4790 // Parent variable as first step
4791 let parent_data = self.context.get_parent().ok_or_else(|| {
4792 EvaluatorError::ReferenceError("Parent context not available".to_string())
4793 })?;
4794
4795 if name.is_empty() {
4796 // $$ alone returns parent context
4797 parent_data.clone()
4798 } else {
4799 // $$field accesses field on parent
4800 match parent_data {
4801 JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4802 _ => JValue::Null,
4803 }
4804 }
4805 }
4806 AstNode::Name(field_name) => {
4807 // Field/property access - get the stages for this step
4808 let stages = &steps[0].stages;
4809
4810 match data {
4811 JValue::Object(obj) => {
4812 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4813 // Apply any stages to the extracted value
4814 if !stages.is_empty() {
4815 self.apply_stages(val, stages)?
4816 } else {
4817 val
4818 }
4819 }
4820 #[cfg(feature = "python")]
4821 JValue::LazyPyDict(lazy) => {
4822 let val = lazy.get_field(field_name)?;
4823 if !stages.is_empty() {
4824 self.apply_stages(val, stages)?
4825 } else {
4826 val
4827 }
4828 }
4829 JValue::Array(arr) => {
4830 // Array mapping: extract field from each element and apply stages
4831 let mut result = Vec::new();
4832 for item in arr.iter() {
4833 match item {
4834 JValue::Object(obj) => {
4835 let val = obj
4836 .get(field_name)
4837 .cloned()
4838 .unwrap_or(JValue::Undefined);
4839 if !val.is_null() && !val.is_undefined() {
4840 if !stages.is_empty() {
4841 // Apply stages to the extracted value
4842 let processed_val =
4843 self.apply_stages(val, stages)?;
4844 // Stages always return an array (or null); extend results
4845 match processed_val {
4846 JValue::Array(arr) => {
4847 result.extend(arr.iter().cloned())
4848 }
4849 JValue::Null => {} // Skip nulls from stage application
4850 other => result.push(other), // Shouldn't happen, but handle it
4851 }
4852 } else {
4853 // No stages: flatten arrays, push scalars
4854 match val {
4855 JValue::Array(arr) => {
4856 result.extend(arr.iter().cloned())
4857 }
4858 other => result.push(other),
4859 }
4860 }
4861 }
4862 }
4863 JValue::Array(inner_arr) => {
4864 // Recursively map over nested array
4865 let nested_result = self.evaluate_path(
4866 &[steps[0].clone()],
4867 &JValue::Array(inner_arr.clone()),
4868 )?;
4869 match nested_result {
4870 JValue::Array(nested) => {
4871 result.extend(nested.iter().cloned())
4872 }
4873 JValue::Null => {} // Skip nulls from nested arrays
4874 other => result.push(other),
4875 }
4876 }
4877 #[cfg(feature = "python")]
4878 JValue::LazyPyDict(lazy) => {
4879 let val = lazy.get_field(field_name)?;
4880 if !val.is_null() && !val.is_undefined() {
4881 if !stages.is_empty() {
4882 let processed_val =
4883 self.apply_stages(val, stages)?;
4884 match processed_val {
4885 JValue::Array(arr) => {
4886 result.extend(arr.iter().cloned())
4887 }
4888 JValue::Null => {}
4889 other => result.push(other),
4890 }
4891 } else {
4892 match val {
4893 JValue::Array(arr) => {
4894 result.extend(arr.iter().cloned())
4895 }
4896 other => result.push(other),
4897 }
4898 }
4899 }
4900 }
4901 _ => {} // Skip non-object items
4902 }
4903 }
4904 JValue::array(result)
4905 }
4906 JValue::Null => JValue::Null,
4907 // Accessing field on non-object returns undefined (not an error)
4908 _ => JValue::Undefined,
4909 }
4910 }
4911 AstNode::String(string_literal) => {
4912 // String literal in path context - evaluate as literal and apply stages
4913 // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4914 let stages = &steps[0].stages;
4915 let val = JValue::string(string_literal.clone());
4916
4917 if !stages.is_empty() {
4918 // Apply stages (predicates) to the string literal
4919 let result = self.apply_stages(val, stages)?;
4920 // Unwrap single-element arrays back to scalar
4921 // (string literals with predicates should return scalar or null, not arrays)
4922 match result {
4923 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4924 JValue::Array(arr) if arr.is_empty() => JValue::Null,
4925 other => other,
4926 }
4927 } else {
4928 val
4929 }
4930 }
4931 AstNode::Predicate(pred_expr) => {
4932 // Predicate as first step
4933 self.evaluate_predicate(data, pred_expr)?
4934 }
4935 _ => {
4936 // Complex first step - evaluate it. When the step is
4937 // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4938 // whose `Product` is `%`-tagged, as in
4939 // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4940 // inner path's tuple wrappers so the following predicate/step
4941 // can read the `!label` bindings.
4942 let saved_keep = self.keep_tuple_stream;
4943 if steps[0].is_tuple {
4944 self.keep_tuple_stream = true;
4945 }
4946 let v = self.evaluate_path_step(&steps[0].node, data, data);
4947 self.keep_tuple_stream = saved_keep;
4948 v?
4949 }
4950 }
4951 };
4952
4953 // Process remaining steps
4954 for (step_idx, step) in steps[1..].iter().enumerate() {
4955 let is_last_step = step_idx == steps.len() - 2;
4956 // Early return if current is null/undefined - no point continuing
4957 // This handles cases like `blah.{}` where blah doesn't exist
4958 if current.is_null() {
4959 return Ok(JValue::Null);
4960 }
4961 if current.is_undefined() {
4962 return Ok(JValue::Undefined);
4963 }
4964
4965 // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4966 // a tuple stream, which selects a single tuple and unwraps it out of
4967 // the array) must stay a tuple stream so the following step keeps
4968 // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4969 // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4970 if let JValue::Object(o) = ¤t {
4971 if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4972 current = JValue::array(vec![current.clone()]);
4973 // The lone wrapper came from a singleton index selection, so
4974 // the final result should unwrap back to a scalar (a following
4975 // object step must not leave a spurious 1-element array).
4976 did_array_mapping = true;
4977 }
4978 }
4979
4980 // Check if current is a tuple array - if so, we need to bind tuple variables
4981 // to context so they're available in nested expressions (like predicates)
4982 let is_tuple_array = if let JValue::Array(arr) = ¤t {
4983 arr.first().is_some_and(|first| {
4984 if let JValue::Object(obj) = first {
4985 obj.get("__tuple__") == Some(&JValue::Bool(true))
4986 } else {
4987 false
4988 }
4989 })
4990 } else {
4991 false
4992 };
4993
4994 // Tuple-binding step (@ focus / # index / % parent): create/extend the
4995 // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4996 // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4997 // wrappers via the existing tuple-aware handling below.
4998 //
4999 // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
5000 // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
5001 // stream, but ONLY when it is consuming an existing tuple stream:
5002 // its ancestor label lives in those incoming tuples, so
5003 // create_tuple_stream's per-tuple frame binding is what lets
5004 // `evaluate_internal(Parent, ..)` resolve it (and any predicate
5005 // stage on the `%` step then resolves in the same frame). A `%`
5006 // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
5007 // predicate, whose input is plain data, not a tuple stream) must
5008 // NOT be routed here -- it's an ordinary scope lookup.
5009 let is_parent_step_over_tuple =
5010 matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
5011 if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
5012 current = JValue::array(self.create_tuple_stream(step, ¤t, false)?);
5013 continue;
5014 }
5015
5016 // For tuple arrays with certain step types, we need special handling to bind
5017 // tuple variables to context so they're available in nested expressions.
5018 // This is needed for:
5019 // - Object constructors: {"label": $$.items[$i]} needs $i in context
5020 // - Function applications: .($$.items[$i]) needs $i in context
5021 // - Variable lookups: .$i needs to find the tuple binding
5022 //
5023 // Steps like Name (field access) already have proper tuple handling in their
5024 // specific cases, so we don't intercept those here.
5025 let needs_tuple_context_binding = is_tuple_array
5026 && matches!(
5027 &step.node,
5028 AstNode::Object(_)
5029 | AstNode::FunctionApplication(_)
5030 | AstNode::Variable(_)
5031 | AstNode::ArrayGroup(_)
5032 );
5033
5034 if needs_tuple_context_binding {
5035 if let JValue::Array(arr) = ¤t {
5036 let mut results = Vec::new();
5037
5038 for tuple in arr.iter() {
5039 if let JValue::Object(tuple_obj) = tuple {
5040 // Extract tuple bindings so nested expressions can see
5041 // them: `$var` focus/index bindings (stored `$name`,
5042 // bound as `name`) AND `!label` ancestor bindings for
5043 // `%` (stored and bound under the full `!label` key).
5044 // Saves/restores rather than blindly unbinding, so a
5045 // tuple key that collides with a live outer `:=`
5046 // binding doesn't get deleted afterward.
5047 let tuple_bindings = self.bind_tuple_keys(tuple_obj);
5048
5049 // Get the actual value from the tuple (@ field)
5050 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
5051
5052 // Evaluate the step
5053 let step_result = match &step.node {
5054 AstNode::Variable(_) => {
5055 // Variable lookup - check context (which now has bindings)
5056 self.evaluate_internal(&step.node, tuple)?
5057 }
5058 AstNode::Object(_) | AstNode::ArrayGroup(_) => {
5059 // Object / array constructor step (e.g.
5060 // `Product.[`Product Name`, %.OrderID]`) -
5061 // evaluate on the tuple's `@` value with the
5062 // carried `!label`/`$focus` bindings in scope
5063 // so an embedded `%` resolves.
5064 self.evaluate_internal(&step.node, &actual_data)?
5065 }
5066 AstNode::FunctionApplication(inner) => {
5067 // A parenthesized step `(expr)` consuming a tuple stream
5068 // (e.g. `Account.Order.Product.( %.OrderID )` or
5069 // `Employee@$e.(Contact)[...]`): evaluate the INNER
5070 // expression on the tuple's `@` value with `$` bound to
5071 // it, mirroring the non-tuple FunctionApplication step
5072 // handling. Routing the wrapper node itself through
5073 // evaluate_internal raises "Function application can only
5074 // be used in path expressions".
5075 let saved_dollar = self.context.lookup("$").cloned();
5076 self.context.bind("$".to_string(), actual_data.clone());
5077 // Keep tuple wrappers from the inner path alive:
5078 // when `inner` is itself a tuple-carrying path
5079 // (e.g. `(Order.Product)` whose `Product` is
5080 // `%`-tagged), its `!label` wrappers must survive
5081 // to be merged into this tuple by the rewrap below
5082 // (they feed a later `%`/`%.%`). Without this the
5083 // inner path projects to `@` and drops the labels.
5084 let saved_keep = self.keep_tuple_stream;
5085 self.keep_tuple_stream = true;
5086 let v = self.evaluate_internal(inner, &actual_data);
5087 self.keep_tuple_stream = saved_keep;
5088 match saved_dollar {
5089 Some(s) => self.context.bind("$".to_string(), s),
5090 None => self.context.unbind("$"),
5091 }
5092 v?
5093 }
5094 _ => unreachable!(), // We only match specific types above
5095 };
5096
5097 // Apply this step's own filter stages (e.g. the
5098 // `[$substring(title,0,3)='The']` on `.$[...]` in
5099 // `library.books#$pos.$[...].$pos`) while the tuple
5100 // bindings are still in scope, so the predicate can
5101 // reference them and non-matching tuples are dropped.
5102 let step_result = if step.stages.is_empty() {
5103 step_result
5104 } else {
5105 self.apply_stages(step_result, &step.stages)?
5106 };
5107
5108 // Restore previous bindings
5109 tuple_bindings.restore(self);
5110
5111 // Rewrap results as tuples carrying this incoming
5112 // tuple's focus/index/ancestor bindings, so that
5113 // DOWNSTREAM steps keep seeing them: a predicate like
5114 // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
5115 // later `%`/`%.%` in `Account.Order.(Product).{...}`,
5116 // or a further path step all read those bindings from
5117 // the tuple wrapper (see AstNode::Variable's tuple
5118 // fallback). Without rewrapping, the tuple chain is
5119 // severed after a parenthesized/object/variable step
5120 // and those references resolve to nothing. The
5121 // wrappers are projected back to their `@` values by
5122 // the top-level `unwrap_tuple_output` pass.
5123 let carried: Vec<(String, JValue)> = tuple_obj
5124 .iter()
5125 .filter(|(k, _)| {
5126 (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
5127 })
5128 .map(|(k, v)| (k.clone(), v.clone()))
5129 .collect();
5130 let wrap = |v: JValue| -> JValue {
5131 match v {
5132 // If the step produced a nested tuple stream
5133 // (e.g. `(Product)` whose inner `Product` is
5134 // itself `%`-tagged), MERGE the inner tuple's
5135 // keys over the carried outer bindings, mirroring
5136 // jsonata-js's `res.tupleStream` branch
5137 // (`Object.assign(tuple, res[bb])`) -- do NOT
5138 // double-wrap, which would bury `@`/`!label`
5139 // one level down and break a following `%`/`%.%`.
5140 JValue::Object(inner)
5141 if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
5142 {
5143 let mut w = IndexMap::new();
5144 for (k, val) in &carried {
5145 w.insert(k.clone(), val.clone());
5146 }
5147 for (k, val) in inner.iter() {
5148 w.insert(k.clone(), val.clone());
5149 }
5150 w.insert("__tuple__".to_string(), JValue::Bool(true));
5151 JValue::object(w)
5152 }
5153 other => {
5154 let mut w = IndexMap::new();
5155 w.insert("@".to_string(), other);
5156 for (k, val) in &carried {
5157 w.insert(k.clone(), val.clone());
5158 }
5159 w.insert("__tuple__".to_string(), JValue::Bool(true));
5160 JValue::object(w)
5161 }
5162 }
5163 };
5164 if !step_result.is_null() && !step_result.is_undefined() {
5165 // Object constructors yield one value per tuple;
5166 // other steps may yield an array to splice in.
5167 if matches!(&step.node, AstNode::Object(_)) {
5168 results.push(wrap(step_result));
5169 } else if let JValue::Array(arr) = step_result {
5170 for it in arr.iter() {
5171 results.push(wrap(it.clone()));
5172 }
5173 } else {
5174 results.push(wrap(step_result));
5175 }
5176 }
5177 }
5178 }
5179
5180 current = JValue::array(results);
5181 continue; // Skip the regular step processing
5182 }
5183 }
5184
5185 current = match &step.node {
5186 AstNode::Wildcard => {
5187 // Wildcard in path
5188 let stages = &step.stages;
5189 let normalized_current = normalize_lazy(¤t)?;
5190 let wildcard_result = match &normalized_current {
5191 JValue::Object(obj) => {
5192 let mut result = Vec::new();
5193 for value in obj.values() {
5194 // Flatten arrays into the result
5195 match value {
5196 JValue::Array(arr) => result.extend(arr.iter().cloned()),
5197 _ => result.push(value.clone()),
5198 }
5199 }
5200 JValue::array(result)
5201 }
5202 JValue::Array(arr) => {
5203 // Map wildcard over array
5204 let mut all_values = Vec::new();
5205 for item in arr.iter() {
5206 let normalized_item = normalize_lazy(item)?;
5207 match &normalized_item {
5208 JValue::Object(obj) => {
5209 for value in obj.values() {
5210 // Flatten arrays
5211 match value {
5212 JValue::Array(arr) => {
5213 all_values.extend(arr.iter().cloned())
5214 }
5215 _ => all_values.push(value.clone()),
5216 }
5217 }
5218 }
5219 JValue::Array(inner) => {
5220 all_values.extend(inner.iter().cloned());
5221 }
5222 _ => {}
5223 }
5224 }
5225 JValue::array(all_values)
5226 }
5227 _ => JValue::Null,
5228 };
5229
5230 // Apply stages (predicates) if present
5231 if !stages.is_empty() {
5232 self.apply_stages(wildcard_result, stages)?
5233 } else {
5234 wildcard_result
5235 }
5236 }
5237 AstNode::Descendant => {
5238 // Descendant in path
5239 match ¤t {
5240 JValue::Array(arr) => {
5241 // Collect descendants from all array elements
5242 let mut all_descendants = Vec::new();
5243 for item in arr.iter() {
5244 all_descendants.extend(self.collect_descendants(item)?);
5245 }
5246 JValue::array(all_descendants)
5247 }
5248 _ => {
5249 // Collect descendants from current value
5250 let descendants = self.collect_descendants(¤t)?;
5251 JValue::array(descendants)
5252 }
5253 }
5254 }
5255 AstNode::Name(field_name) => {
5256 // Navigate into object field or map over array, applying stages
5257 let stages = &step.stages;
5258
5259 match ¤t {
5260 JValue::Object(obj) => {
5261 // Single object field extraction - NOT array mapping
5262 // This resets did_array_mapping because we're extracting from
5263 // a single value, not mapping over an array. The field's value
5264 // (even if it's an array) should be preserved as-is.
5265 did_array_mapping = false;
5266 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
5267 // Apply stages if present
5268 if !stages.is_empty() {
5269 self.apply_stages(val, stages)?
5270 } else {
5271 val
5272 }
5273 }
5274 #[cfg(feature = "python")]
5275 JValue::LazyPyDict(lazy) => {
5276 did_array_mapping = false;
5277 let val = lazy.get_field(field_name)?;
5278 if !stages.is_empty() {
5279 self.apply_stages(val, stages)?
5280 } else {
5281 val
5282 }
5283 }
5284 JValue::Array(arr) => {
5285 // Array mapping: extract field from each element and apply stages
5286 did_array_mapping = true; // Track that we did array mapping
5287
5288 // Fast path: if no elements are tuples and no stages,
5289 // skip all tuple checking overhead (common case for products.price etc.)
5290 // Tuples are all-or-nothing (created by index binding #$i),
5291 // so checking only the first element is sufficient.
5292 let has_tuples = arr.first().is_some_and(|item| {
5293 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
5294 });
5295
5296 if !has_tuples && stages.is_empty() {
5297 let mut result = Vec::with_capacity(arr.len());
5298 for item in arr.iter() {
5299 match item {
5300 JValue::Object(obj) => {
5301 if let Some(val) = obj.get(field_name) {
5302 if !val.is_null() {
5303 match val {
5304 JValue::Array(arr_val) => {
5305 result.extend(arr_val.iter().cloned())
5306 }
5307 other => result.push(other.clone()),
5308 }
5309 }
5310 }
5311 }
5312 JValue::Array(_) => {
5313 let nested_result =
5314 self.evaluate_path(&[step.clone()], item)?;
5315 match nested_result {
5316 JValue::Array(nested) => {
5317 result.extend(nested.iter().cloned())
5318 }
5319 JValue::Null => {}
5320 other => result.push(other),
5321 }
5322 }
5323 #[cfg(feature = "python")]
5324 JValue::LazyPyDict(lazy) => {
5325 let val = lazy.get_field(field_name)?;
5326 if !val.is_null() && !val.is_undefined() {
5327 match val {
5328 JValue::Array(arr_val) => {
5329 result.extend(arr_val.iter().cloned())
5330 }
5331 other => result.push(other),
5332 }
5333 }
5334 }
5335 _ => {}
5336 }
5337 }
5338 JValue::array(result)
5339 } else {
5340 // Full path with tuple support and stages
5341 let mut result = Vec::new();
5342
5343 for item in arr.iter() {
5344 match item {
5345 JValue::Object(obj) => {
5346 // Check if this is a tuple stream element
5347 let (val, tuple_bindings) = if obj.get("__tuple__")
5348 == Some(&JValue::Bool(true))
5349 {
5350 // This is a tuple - extract '@' value and preserve bindings
5351 // Collect index bindings (variables starting with $)
5352 let bindings: Vec<(String, JValue)> = obj
5353 .iter()
5354 .filter(|(k, _)| k.starts_with('$'))
5355 .map(|(k, v)| (k.clone(), v.clone()))
5356 .collect();
5357 match obj.get("@") {
5358 Some(JValue::Object(inner)) => (
5359 inner
5360 .get(field_name)
5361 .cloned()
5362 .unwrap_or(JValue::Null),
5363 Some(bindings),
5364 ),
5365 #[cfg(feature = "python")]
5366 Some(JValue::LazyPyDict(lazy)) => {
5367 let v = lazy.get_field(field_name)?;
5368 (
5369 if v.is_undefined() {
5370 JValue::Null
5371 } else {
5372 v
5373 },
5374 Some(bindings),
5375 )
5376 }
5377 _ => continue, // Invalid tuple
5378 }
5379 } else {
5380 (
5381 obj.get(field_name)
5382 .cloned()
5383 .unwrap_or(JValue::Null),
5384 None,
5385 )
5386 };
5387
5388 if !val.is_null() {
5389 // Helper to wrap value in tuple if we have bindings
5390 let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
5391 if let Some(b) = bindings {
5392 let mut wrapper = IndexMap::new();
5393 wrapper.insert("@".to_string(), v);
5394 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5395 for (k, val) in b {
5396 wrapper.insert(k.clone(), val.clone());
5397 }
5398 JValue::object(wrapper)
5399 } else {
5400 v
5401 }
5402 };
5403
5404 if !stages.is_empty() {
5405 // Bind this tuple's carried focus/index/ancestor
5406 // bindings so a filter predicate that references
5407 // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
5408 // where the `[$l.isbn=isbn]` stage on the (non-focus)
5409 // `books` step must see `$l` from the enclosing
5410 // `@$l` focus stream. Without this the predicate
5411 // evaluates `$l` as unbound and filters everything out.
5412 let saved_tuple: Vec<(String, Option<JValue>)> =
5413 obj.iter()
5414 .filter_map(|(k, _)| {
5415 if let Some(n) = k.strip_prefix('$')
5416 {
5417 (!n.is_empty())
5418 .then(|| n.to_string())
5419 } else if k.starts_with('!') {
5420 Some(k.clone())
5421 } else {
5422 None
5423 }
5424 })
5425 .map(|n| {
5426 (
5427 n.clone(),
5428 self.context
5429 .lookup(&n)
5430 .cloned(),
5431 )
5432 })
5433 .collect();
5434 for (k, v) in obj.iter() {
5435 if let Some(n) = k.strip_prefix('$') {
5436 if !n.is_empty() {
5437 self.context
5438 .bind(n.to_string(), v.clone());
5439 }
5440 } else if k.starts_with('!') {
5441 self.context.bind(k.clone(), v.clone());
5442 }
5443 }
5444 // Apply stages to the extracted value
5445 let processed_val =
5446 self.apply_stages(val, stages);
5447 for (n, old) in saved_tuple.into_iter().rev() {
5448 match old {
5449 Some(v) => self.context.bind(n, v),
5450 None => self.context.unbind(&n),
5451 }
5452 }
5453 let processed_val = processed_val?;
5454 // Stages always return an array (or null); extend results
5455 match processed_val {
5456 JValue::Array(arr) => {
5457 for item in arr.iter() {
5458 result.push(wrap_in_tuple(
5459 item.clone(),
5460 &tuple_bindings,
5461 ));
5462 }
5463 }
5464 JValue::Null => {} // Skip nulls from stage application
5465 other => result.push(wrap_in_tuple(
5466 other,
5467 &tuple_bindings,
5468 )),
5469 }
5470 } else {
5471 // No stages: flatten arrays, push scalars
5472 // But preserve tuple bindings!
5473 match val {
5474 JValue::Array(arr) => {
5475 for item in arr.iter() {
5476 result.push(wrap_in_tuple(
5477 item.clone(),
5478 &tuple_bindings,
5479 ));
5480 }
5481 }
5482 other => result.push(wrap_in_tuple(
5483 other,
5484 &tuple_bindings,
5485 )),
5486 }
5487 }
5488 }
5489 }
5490 JValue::Array(_) => {
5491 // Recursively map over nested array
5492 let nested_result =
5493 self.evaluate_path(&[step.clone()], item)?;
5494 match nested_result {
5495 JValue::Array(nested) => {
5496 result.extend(nested.iter().cloned())
5497 }
5498 JValue::Null => {}
5499 other => result.push(other),
5500 }
5501 }
5502 #[cfg(feature = "python")]
5503 JValue::LazyPyDict(lazy) => {
5504 // Lazy dicts are never tuples; read directly. Mirrors
5505 // the Object arm's non-tuple branch (tuple_bindings =
5506 // None throughout, so wrap_in_tuple would be a no-op).
5507 let val = lazy.get_field(field_name)?;
5508
5509 if !val.is_null() && !val.is_undefined() {
5510 if !stages.is_empty() {
5511 let processed_val =
5512 self.apply_stages(val, stages)?;
5513 match processed_val {
5514 JValue::Array(arr) => {
5515 result.extend(arr.iter().cloned())
5516 }
5517 JValue::Null => {} // Skip nulls from stage application
5518 other => result.push(other),
5519 }
5520 } else {
5521 match val {
5522 JValue::Array(arr) => {
5523 result.extend(arr.iter().cloned())
5524 }
5525 other => result.push(other),
5526 }
5527 }
5528 }
5529 }
5530 _ => {}
5531 }
5532 }
5533
5534 JValue::array(result)
5535 }
5536 }
5537 JValue::Null => JValue::Null,
5538 // Accessing field on non-object returns undefined (not an error)
5539 _ => JValue::Undefined,
5540 }
5541 }
5542 AstNode::String(string_literal) => {
5543 // String literal as a path step - evaluate as literal and apply stages
5544 let stages = &step.stages;
5545 let val = JValue::string(string_literal.clone());
5546
5547 if !stages.is_empty() {
5548 // Apply stages (predicates) to the string literal
5549 let result = self.apply_stages(val, stages)?;
5550 // Unwrap single-element arrays back to scalar
5551 match result {
5552 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
5553 JValue::Array(arr) if arr.is_empty() => JValue::Null,
5554 other => other,
5555 }
5556 } else {
5557 val
5558 }
5559 }
5560 AstNode::Predicate(pred_expr) => {
5561 // Predicate in path - filter or index into current value
5562 self.evaluate_predicate(¤t, pred_expr)?
5563 }
5564 AstNode::ArrayGroup(elements) => {
5565 // Array grouping: map expression over array but keep results grouped
5566 // .[expr] means evaluate expr for each array element
5567 match ¤t {
5568 JValue::Array(arr) => {
5569 let mut result = Vec::new();
5570 for item in arr.iter() {
5571 // For each array item, evaluate all elements and collect results
5572 let mut group_values = Vec::new();
5573 for element in elements {
5574 let value = self.evaluate_internal(element, item)?;
5575 // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
5576 // This ensures [[expr]] produces properly nested arrays
5577 let should_preserve_array = matches!(
5578 element,
5579 AstNode::Array(_) | AstNode::ArrayGroup(_)
5580 );
5581
5582 if should_preserve_array {
5583 // Keep the array as a single element to preserve nesting
5584 group_values.push(value);
5585 } else {
5586 // Flatten the value into group_values
5587 match value {
5588 JValue::Array(arr) => {
5589 group_values.extend(arr.iter().cloned())
5590 }
5591 other => group_values.push(other),
5592 }
5593 }
5594 }
5595 // Each array element gets its own sub-array with all values
5596 result.push(JValue::array(group_values));
5597 }
5598 // jsonata-js's evaluateStep: when this is the path's last
5599 // step and mapping produced exactly one constructed
5600 // sub-array, that sub-array IS the path result directly
5601 // (not wrapped in an outer singleton array) — e.g.
5602 // `$.[value,epochSeconds]` over a 1-element array yields
5603 // `[3, 1578381600]`, not `[[3, 1578381600]]`.
5604 if is_last_step && result.len() == 1 {
5605 result.into_iter().next().unwrap()
5606 } else {
5607 JValue::array(result)
5608 }
5609 }
5610 _ => {
5611 // For non-arrays, just evaluate the array constructor normally
5612 let mut result = Vec::new();
5613 for element in elements {
5614 let value = self.evaluate_internal(element, ¤t)?;
5615 result.push(value);
5616 }
5617 JValue::array(result)
5618 }
5619 }
5620 }
5621 AstNode::FunctionApplication(expr) => {
5622 // Function application: map expr over the current value
5623 // .(expr) means evaluate expr for each element, with $ bound to that element
5624 // Null/undefined results are filtered out
5625 //
5626 // When this parenthesized step is itself tuple-carrying (its
5627 // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
5628 // keep the inner path's tuple wrappers so their `!label`
5629 // bindings survive to the following object/`%` step; the
5630 // end-of-path projection (or a later consumer) unwraps them.
5631 let saved_keep = self.keep_tuple_stream;
5632 if step.is_tuple {
5633 self.keep_tuple_stream = true;
5634 }
5635 let fa_result = match ¤t {
5636 JValue::Array(arr) => {
5637 // Produce the mapped result (compiled fast path or tree-walker fallback).
5638 // Do NOT return early — singleton unwrapping is applied by the outer
5639 // path evaluation code after all steps are processed.
5640 let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
5641 {
5642 let shape = arr.first().and_then(build_shape_cache);
5643 let mut result = Vec::with_capacity(arr.len());
5644 for item in arr.iter() {
5645 let value = if let Some(ref s) = shape {
5646 eval_compiled_shaped(
5647 &compiled,
5648 item,
5649 None,
5650 s,
5651 &self.options,
5652 self.start_time,
5653 )?
5654 } else {
5655 eval_compiled(
5656 &compiled,
5657 item,
5658 None,
5659 &self.options,
5660 self.start_time,
5661 )?
5662 };
5663 if !value.is_null() && !value.is_undefined() {
5664 result.push(value);
5665 }
5666 }
5667 result
5668 } else {
5669 let mut result = Vec::new();
5670 for item in arr.iter() {
5671 // Save the current $ binding
5672 let saved_dollar = self.context.lookup("$").cloned();
5673
5674 // Bind $ to the current item
5675 self.context.bind("$".to_string(), item.clone());
5676
5677 // Evaluate the expression in the context of this item
5678 let value = self.evaluate_internal(expr, item)?;
5679
5680 // Restore the previous $ binding
5681 if let Some(saved) = saved_dollar {
5682 self.context.bind("$".to_string(), saved);
5683 } else {
5684 self.context.unbind("$");
5685 }
5686
5687 // Only include non-null/undefined values
5688 if !value.is_null() && !value.is_undefined() {
5689 result.push(value);
5690 }
5691 }
5692 result
5693 };
5694 // Don't do singleton unwrapping here - let the path result
5695 // handling deal with it, which respects has_explicit_array_keep
5696 JValue::array(mapped)
5697 }
5698 _ => {
5699 // For non-arrays, bind $ and evaluate
5700 let saved_dollar = self.context.lookup("$").cloned();
5701 self.context.bind("$".to_string(), current.clone());
5702
5703 let value = self.evaluate_internal(expr, ¤t)?;
5704
5705 if let Some(saved) = saved_dollar {
5706 self.context.bind("$".to_string(), saved);
5707 } else {
5708 self.context.unbind("$");
5709 }
5710
5711 value
5712 }
5713 };
5714 self.keep_tuple_stream = saved_keep;
5715 fa_result
5716 }
5717 AstNode::Sort { terms, .. } => {
5718 // Sort as a path step - sort 'current' by the terms
5719 self.evaluate_sort(¤t, terms)?
5720 }
5721 // Handle complex path steps (e.g., computed properties, object construction)
5722 _ => {
5723 let saved_keep = self.keep_tuple_stream;
5724 if step.is_tuple {
5725 self.keep_tuple_stream = true;
5726 }
5727 let v = self.evaluate_path_step(&step.node, ¤t, data);
5728 self.keep_tuple_stream = saved_keep;
5729 v?
5730 }
5731 };
5732 }
5733
5734 // End-of-path tuple projection, mirroring jsonata-js evaluatePath
5735 // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
5736 // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
5737 // wrappers are internal bookkeeping and must not escape into an enclosing
5738 // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
5739 // `=` compare wrapper objects and always yield false). Suppressed only for
5740 // the two consumers that read the carried bindings directly off the
5741 // wrappers (Sort input, ObjectTransform/group-by input), which set
5742 // `keep_tuple_stream`. The top-level `evaluate()` still runs
5743 // `unwrap_tuple_output` as a backstop for wrappers nested inside
5744 // constructed output.
5745 if !self.keep_tuple_stream {
5746 if let JValue::Array(arr) = ¤t {
5747 let is_tuple_stream = arr.first().is_some_and(|f| {
5748 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5749 });
5750 if is_tuple_stream {
5751 let projected: Vec<JValue> = arr
5752 .iter()
5753 .map(|t| match t {
5754 JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
5755 other => other.clone(),
5756 })
5757 .collect();
5758 current = JValue::array(projected);
5759 }
5760 }
5761 }
5762
5763 // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5764 // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5765
5766 // Check for explicit array-keeping operations. Empty predicate `[]` can
5767 // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5768 // stage; it also counts when it sits inside a `Sort` step's input path
5769 // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5770 // the sort and the trailing index so the singleton stays `[4]`.
5771 let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5772
5773 // Unwrap when:
5774 // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5775 // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5776 // Note: did_array_mapping is reset to false when extracting from a single object,
5777 // so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5778 // BUT NOT when there's an explicit array-keeping operation
5779 //
5780 // Important: We DON'T unwrap just because original data was an array - what matters is
5781 // whether the final extraction was from an array mapping context or a single object.
5782 let should_unwrap = !has_explicit_array_keep
5783 && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5784
5785 let result = match ¤t {
5786 // An empty result sequence is "no value" -> undefined (jsonata-js
5787 // treats an empty sequence, e.g. from a filter that matched nothing,
5788 // as undefined so a following `.field` and object/array construction
5789 // drop it rather than keeping an explicit null). `[]` array-keep is
5790 // handled separately above via has_explicit_array_keep.
5791 JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5792 // Unwrap singleton arrays when appropriate
5793 JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5794 // Keep arrays otherwise
5795 _ => current,
5796 };
5797
5798 // An explicit `[]` keep-array forces the result to remain an array even
5799 // after a later singleton index collapses it to a scalar (jsonata's
5800 // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5801 let result = if has_explicit_array_keep
5802 && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5803 {
5804 JValue::array(vec![result])
5805 } else {
5806 result
5807 };
5808
5809 if let JValue::Array(arr) = &result {
5810 check_sequence_length(arr.len(), &self.options)?;
5811 }
5812
5813 Ok(result)
5814 }
5815
5816 /// True when a path step carries a tuple-binding flag (`@$var` focus,
5817 /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5818 /// produce/extend a tuple stream rather than be evaluated as a plain step.
5819 ///
5820 fn step_creates_tuple(step: &PathStep) -> bool {
5821 step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5822 }
5823
5824 /// True when a path contains an explicit empty predicate `[]` (keep-array),
5825 /// either directly as a step/stage or nested inside a `Sort` step's input
5826 /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5827 /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5828 /// -> `[4]`).
5829 fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5830 steps.iter().any(|step| {
5831 if let AstNode::Predicate(pred) = &step.node {
5832 if matches!(**pred, AstNode::Boolean(true)) {
5833 return true;
5834 }
5835 }
5836 if step.stages.iter().any(
5837 |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5838 ) {
5839 return true;
5840 }
5841 if let AstNode::Sort { input, .. } = &step.node {
5842 if let AstNode::Path { steps: inner } = input.as_ref() {
5843 return Self::path_keeps_singleton_array(inner);
5844 }
5845 }
5846 false
5847 })
5848 }
5849
5850 /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5851 /// scope, saving whatever was previously bound under each of those names
5852 /// so [`TupleKeyBindings::restore`] can put it back afterward.
5853 ///
5854 /// This is the single shared implementation of the
5855 /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5856 /// pattern that recurs across `create_tuple_stream`,
5857 /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5858 /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5859 /// none of those call sites can regress to a blind `unbind` (which
5860 /// deletes rather than restores a same-named outer `:=` binding that was
5861 /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5862 /// binding silently clobbering an outer variable of the same name).
5863 fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5864 let mut saved = Vec::new();
5865 for (key, value) in tuple_obj.iter() {
5866 let name = if let Some(n) = key.strip_prefix('$') {
5867 if n.is_empty() {
5868 continue;
5869 }
5870 n.to_string()
5871 } else if key.starts_with('!') {
5872 key.clone()
5873 } else {
5874 continue;
5875 };
5876 saved.push((name.clone(), self.context.lookup(&name).cloned()));
5877 self.context.bind(name, value.clone());
5878 }
5879 TupleKeyBindings { saved }
5880 }
5881
5882 /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5883 /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5884 /// vector holds `JValue::Object` tuple wrappers of the shape
5885 /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5886 /// which downstream steps consume via the existing tuple-aware handling in
5887 /// `evaluate_path`.
5888 ///
5889 /// `input` is the previous step's result: either an already-built tuple
5890 /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5891 /// plain value/array entering tuple mode for the first time (each item
5892 /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5893 ///
5894 /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5895 /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5896 /// fast paths only *rebuild* a wrapper around a value pulled from an input
5897 /// element that is already `__tuple__`-tagged, which can only be true if a
5898 /// `create_tuple_stream` call already ran earlier in this evaluation and set
5899 /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5900 /// fire on a value that did NOT come from an existing tuple stream, it must
5901 /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5902 /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5903 fn create_tuple_stream(
5904 &mut self,
5905 step: &PathStep,
5906 input: &JValue,
5907 is_first_path_step: bool,
5908 ) -> Result<Vec<JValue>, EvaluatorError> {
5909 use std::rc::Rc;
5910
5911 // Mark that this evaluate() call produced tuple wrappers, so the
5912 // top-level `evaluate()` knows to run the output-unwrap pass.
5913 self.tuple_stream_created = true;
5914
5915 // Gather the incoming tuple bindings.
5916 let is_tuple_input = matches!(
5917 input,
5918 JValue::Array(arr) if arr.first().is_some_and(|f| {
5919 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5920 })
5921 );
5922 let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5923 match input {
5924 JValue::Array(arr) => arr
5925 .iter()
5926 .filter_map(|t| match t {
5927 JValue::Object(o) => Some(o.clone()),
5928 _ => None,
5929 })
5930 .collect(),
5931 _ => unreachable!(),
5932 }
5933 } else {
5934 let items: Vec<JValue> = match input {
5935 // Mirrors jsonata-js evaluatePath's inputSequence rule
5936 // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5937 // when the path's FIRST step is a variable reference (`$`/`$$`) the
5938 // input array is taken as a SINGLE sequence value
5939 // (`createSequence(input)`) rather than iterated per-element. We
5940 // only need this for a leading INDEX bind (`$#$pos`): the whole
5941 // array becomes one incoming tuple whose `@` is the array, then
5942 // the inner position counter walks its elements so `$pos` runs
5943 // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5944 // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5945 // the step input, so a single binding would yield one copy of the
5946 // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5947 // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5948 // `$.$#$pos` (a later step) still iterates per-element.
5949 JValue::Array(arr)
5950 if !(is_first_path_step
5951 && matches!(&step.node, AstNode::Variable(_))
5952 && step.index_var.is_some()) =>
5953 {
5954 arr.iter().cloned().collect()
5955 }
5956 single => vec![single.clone()],
5957 };
5958 items
5959 .into_iter()
5960 .map(|item| {
5961 let mut wrapper = IndexMap::new();
5962 wrapper.insert("@".to_string(), item);
5963 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5964 Rc::new(wrapper)
5965 })
5966 .collect()
5967 };
5968
5969 // A sort step in a tuple stream orders the WHOLE stream (not per element)
5970 // and re-tuples with the index = sorted position, mirroring jsonata-js
5971 // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5972 // array, then number the sorted values, then filter by `$pos`.
5973 if let AstNode::Sort { terms, .. } = &step.node {
5974 let stream = JValue::array(
5975 incoming
5976 .iter()
5977 .map(|t| JValue::object((**t).clone()))
5978 .collect(),
5979 );
5980 // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5981 // carried keys bound), returning the wrappers in sorted order.
5982 let sorted = self.evaluate_sort(&stream, terms)?;
5983 let sorted_arr: Vec<JValue> = match sorted {
5984 JValue::Array(a) => a.iter().cloned().collect(),
5985 JValue::Null | JValue::Undefined => Vec::new(),
5986 other => vec![other],
5987 };
5988 let mut result = Vec::new();
5989 for (ss, elem) in sorted_arr.into_iter().enumerate() {
5990 let mut new_tuple = match elem {
5991 JValue::Object(o) => (*o).clone(),
5992 other => {
5993 let mut m = IndexMap::new();
5994 m.insert("@".to_string(), other);
5995 m
5996 }
5997 };
5998 if let Some(index_var) = &step.index_var {
5999 new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
6000 }
6001 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
6002 result.push(JValue::object(new_tuple));
6003 }
6004 return Ok(result);
6005 }
6006
6007 let mut result = Vec::new();
6008 for tuple_obj in incoming {
6009 // Bind every carried tuple key into a real scope frame so the step
6010 // expression can see prior focus/index/ancestor bindings, mirroring
6011 // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
6012 // Saves/restores rather than blindly unbinding, so a tuple key
6013 // whose name collides with a live outer `:=` binding doesn't get
6014 // deleted once this tuple row's evaluation is done.
6015 let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
6016
6017 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
6018 let step_value = self.evaluate_internal(&step.node, &actual_data);
6019
6020 let mut step_value = step_value?;
6021 // When the step carries an ORDERED index stage (a second `#$var`,
6022 // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
6023 // BUILT tuple stream in order (filter then re-number) so the filter
6024 // sees the per-tuple focus/index bindings and each index reflects the
6025 // position at its point in the sequence. Those steps defer all stage
6026 // application to `apply_tuple_stages` after the stream is built.
6027 let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
6028 if !step.stages.is_empty() && !has_index_stage {
6029 // A `%` inside a filter predicate refers to the ancestry of
6030 // THIS step (its own input for a level-1 `%`, or an earlier
6031 // step's input for a `%.%` chain). ast_transform tags this step
6032 // with `ancestor_label`; bind it to the step's input so the
6033 // level-1 `%` resolves. The `%.%` chain's deeper references use
6034 // labels carried in the INCOMING tuple, so those bindings
6035 // (`tuple_bindings`) must stay live through `apply_stages` --
6036 // their restore is deferred until after it (previously they
6037 // were unbound first, which silently broke `%.%` inside
6038 // predicates).
6039 let own_label = match &step.ancestor_label {
6040 Some(label) if !tuple_bindings.contains(label) => {
6041 self.context.bind(label.clone(), actual_data.clone());
6042 Some(label.clone())
6043 }
6044 _ => None,
6045 };
6046 step_value = self.apply_stages(step_value, &step.stages)?;
6047 if let Some(label) = own_label {
6048 self.context.unbind(&label);
6049 }
6050 }
6051
6052 tuple_bindings.restore(self);
6053
6054 let row: Vec<JValue> = match step_value {
6055 JValue::Undefined => continue,
6056 JValue::Array(arr) => arr.iter().cloned().collect(),
6057 other => vec![other],
6058 };
6059
6060 for (position, value) in row.into_iter().enumerate() {
6061 if value.is_undefined() {
6062 continue;
6063 }
6064 let mut new_tuple = (*tuple_obj).clone();
6065 if let Some(focus_var) = &step.focus {
6066 // Focus binding keeps `@` as this step's INPUT (already carried
6067 // in the cloned tuple) and binds the result to `$focus`,
6068 // matching jsonata-js: `tuple[expr.focus] = res[bb];
6069 // tuple['@'] = tupleBindings[ee]['@'];`.
6070 new_tuple.insert(format!("${}", focus_var), value);
6071 } else {
6072 new_tuple.insert("@".to_string(), value);
6073 }
6074 if let Some(index_var) = &step.index_var {
6075 // Index binding records the position of this value WITHIN the
6076 // per-binding result row (jsonata-js evaluateTupleStep: the
6077 // inner `bb` counter, `tuple[expr.index] = bb`), which resets
6078 // for each incoming tuple.
6079 new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
6080 }
6081 if let Some(ancestor_label) = &step.ancestor_label {
6082 // `%` ancestor: preserve this step's INPUT under the label.
6083 new_tuple.insert(ancestor_label.clone(), actual_data.clone());
6084 }
6085 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
6086 result.push(JValue::object(new_tuple));
6087 }
6088 }
6089
6090 // Apply ordered filter/index stages to the built tuple stream when a
6091 // second index binding deferred them (see the has_index_stage comment
6092 // in the build loop above).
6093 if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
6094 result = self.apply_tuple_stages(result, &step.stages)?;
6095 }
6096
6097 Ok(result)
6098 }
6099
6100 /// Apply a step's stages, in order, to an already-built tuple stream --
6101 /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
6102 /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
6103 /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
6104 /// stage sets its variable on every surviving tuple to that tuple's position
6105 /// in the CURRENT stream. Used for steps carrying a second `#$var` index
6106 /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
6107 /// pre-filter position and `$ib2` the post-filter position.
6108 fn apply_tuple_stages(
6109 &mut self,
6110 mut tuples: Vec<JValue>,
6111 stages: &[Stage],
6112 ) -> Result<Vec<JValue>, EvaluatorError> {
6113 for stage in stages {
6114 match stage {
6115 Stage::Filter(pred) => {
6116 let mut kept = Vec::with_capacity(tuples.len());
6117 for tup in tuples.into_iter() {
6118 let JValue::Object(obj) = &tup else {
6119 continue;
6120 };
6121 // Bind this tuple's carried focus/index/ancestor keys so
6122 // the predicate can reference them (save/restore rather
6123 // than blind unbind -- see bind_tuple_keys).
6124 let tuple_bindings = self.bind_tuple_keys(obj);
6125 let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
6126 let pred_res = self.evaluate_internal(pred, &at);
6127 tuple_bindings.restore(self);
6128 if self.is_truthy(&pred_res?) {
6129 kept.push(tup);
6130 }
6131 }
6132 tuples = kept;
6133 }
6134 Stage::Index(var) => {
6135 for (pos, tup) in tuples.iter_mut().enumerate() {
6136 if let JValue::Object(obj) = tup {
6137 let mut m = (**obj).clone();
6138 m.insert(format!("${}", var), JValue::from(pos as i64));
6139 *tup = JValue::object(m);
6140 }
6141 }
6142 }
6143 }
6144 }
6145 Ok(tuples)
6146 }
6147
6148 /// Helper to evaluate a complex path step
6149 fn evaluate_path_step(
6150 &mut self,
6151 step: &AstNode,
6152 current: &JValue,
6153 original_data: &JValue,
6154 ) -> Result<JValue, EvaluatorError> {
6155 // Special case: array mapping with object construction
6156 // e.g., items.{"name": name, "price": price}
6157 if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
6158 match (current, step) {
6159 (JValue::Array(arr), AstNode::Object(pairs)) => {
6160 // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
6161 if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
6162 let shape = arr.first().and_then(build_shape_cache);
6163 let mut mapped = Vec::with_capacity(arr.len());
6164 for item in arr.iter() {
6165 let result = if let Some(ref s) = shape {
6166 eval_compiled_shaped(
6167 &compiled,
6168 item,
6169 None,
6170 s,
6171 &self.options,
6172 self.start_time,
6173 )?
6174 } else {
6175 eval_compiled(
6176 &compiled,
6177 item,
6178 None,
6179 &self.options,
6180 self.start_time,
6181 )?
6182 };
6183 if !result.is_undefined() {
6184 mapped.push(result);
6185 }
6186 }
6187 return Ok(JValue::array(mapped));
6188 }
6189 // Fallback: full AST evaluation per element
6190 let mapped: Result<Vec<JValue>, EvaluatorError> = arr
6191 .iter()
6192 .map(|item| self.evaluate_internal(step, item))
6193 .collect();
6194 Ok(JValue::array(mapped?))
6195 }
6196 _ => unreachable!(),
6197 }
6198 } else {
6199 // Special case: array.$ should map $ over the array, returning each element
6200 // e.g., [1, 2, 3].$ returns [1, 2, 3]
6201 if let AstNode::Variable(name) = step {
6202 if name.is_empty() {
6203 // Bare $ - map over array if current is an array
6204 if let JValue::Array(arr) = current {
6205 // Map $ over each element - $ refers to each element in turn
6206 return Ok(JValue::Array(arr.clone()));
6207 } else {
6208 // For non-arrays, $ refers to the current value
6209 return Ok(current.clone());
6210 }
6211 }
6212 }
6213
6214 // Special case: Variable access on tuple arrays (from index binding #$var)
6215 // When current is a tuple array, we need to evaluate the variable against each tuple
6216 // so that tuple bindings ($i, etc.) can be found
6217 if matches!(step, AstNode::Variable(_)) {
6218 if let JValue::Array(arr) = current {
6219 // Check if this is a tuple array
6220 let is_tuple_array = arr.first().is_some_and(|first| {
6221 if let JValue::Object(obj) = first {
6222 obj.get("__tuple__") == Some(&JValue::Bool(true))
6223 } else {
6224 false
6225 }
6226 });
6227
6228 if is_tuple_array {
6229 // Map the variable lookup over each tuple
6230 let mut results = Vec::new();
6231 for tuple in arr.iter() {
6232 // Evaluate the variable in the context of this tuple
6233 // This allows tuple bindings ($i, etc.) to be found
6234 let val = self.evaluate_internal(step, tuple)?;
6235 if !val.is_null() && !val.is_undefined() {
6236 results.push(val);
6237 }
6238 }
6239 return Ok(JValue::array(results));
6240 }
6241 }
6242 }
6243
6244 // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
6245 // rather than being used to index/access the current value
6246 // e.g., items[price > 50] where [price > 50] is a filter operation
6247 // or $x.price where $x is a variable binding
6248 // or $$.field where $$ is the parent context
6249 // or [0..9] where it's an array constructor
6250 // or $^(field) where it's a sort operator
6251 // or (expr).field where (expr) is a block that evaluates to a value
6252 if matches!(
6253 step,
6254 AstNode::Binary { .. }
6255 | AstNode::Function { .. }
6256 | AstNode::Variable(_)
6257 | AstNode::ParentVariable(_)
6258 | AstNode::Parent(_)
6259 | AstNode::Array(_)
6260 | AstNode::Object(_)
6261 | AstNode::Sort { .. }
6262 | AstNode::Block(_)
6263 ) {
6264 // Evaluate the step in the context of original_data and return the result directly
6265 return self.evaluate_internal(step, original_data);
6266 }
6267
6268 // Standard path step evaluation for indexing/accessing current value
6269 let step_value = self.evaluate_internal(step, original_data)?;
6270 Ok(match (current, &step_value) {
6271 (JValue::Object(obj), JValue::String(key)) => {
6272 obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
6273 }
6274 #[cfg(feature = "python")]
6275 (JValue::LazyPyDict(lazy), JValue::String(key)) => lazy.get_field(key)?,
6276 (JValue::Array(arr), JValue::Number(n)) => {
6277 let index = *n as i64;
6278 let len = arr.len() as i64;
6279
6280 // Handle negative indexing (offset from end)
6281 let actual_idx = if index < 0 { len + index } else { index };
6282
6283 if actual_idx < 0 || actual_idx >= len {
6284 JValue::Undefined
6285 } else {
6286 arr[actual_idx as usize].clone()
6287 }
6288 }
6289 _ => JValue::Undefined,
6290 })
6291 }
6292 }
6293
6294 /// Evaluate a binary operation
6295 fn evaluate_binary_op(
6296 &mut self,
6297 op: crate::ast::BinaryOp,
6298 lhs: &AstNode,
6299 rhs: &AstNode,
6300 data: &JValue,
6301 ) -> Result<JValue, EvaluatorError> {
6302 use crate::ast::BinaryOp;
6303
6304 // Special handling for coalescing operator (??)
6305 // Returns right side if left is undefined (produces no value)
6306 // Note: literal null is a value, so it's NOT replaced
6307 if op == BinaryOp::Coalesce {
6308 // Try to evaluate the left side
6309 return match self.evaluate_internal(lhs, data) {
6310 Ok(value) => {
6311 // Successfully evaluated to a value (even if it's null)
6312 // Check if LHS is a literal null - keep it (null is a value, not undefined)
6313 if matches!(lhs, AstNode::Null) {
6314 Ok(value)
6315 }
6316 // For paths and variables, undefined (no match/unbound) - use RHS
6317 else if value.is_undefined()
6318 && (matches!(lhs, AstNode::Path { .. })
6319 || matches!(lhs, AstNode::String(_))
6320 || matches!(lhs, AstNode::Variable(_)))
6321 {
6322 self.evaluate_internal(rhs, data)
6323 } else {
6324 Ok(value)
6325 }
6326 }
6327 Err(_) => {
6328 // Evaluation failed (e.g., undefined variable) - use RHS
6329 self.evaluate_internal(rhs, data)
6330 }
6331 };
6332 }
6333
6334 // Special handling for default operator (?:)
6335 // Returns right side if left is falsy or a non-value (like a function)
6336 if op == BinaryOp::Default {
6337 let left = self.evaluate_internal(lhs, data)?;
6338 if self.is_truthy_for_default(&left) {
6339 return Ok(left);
6340 }
6341 return self.evaluate_internal(rhs, data);
6342 }
6343
6344 // Special handling for chain/pipe operator (~>)
6345 // Pipes the LHS result to the RHS function as the first argument
6346 // e.g., expr ~> func(arg2) becomes func(expr, arg2)
6347 if op == BinaryOp::ChainPipe {
6348 // Handle regex on RHS - treat as $match(lhs, regex)
6349 if let AstNode::Regex { pattern, flags } = rhs {
6350 // Evaluate LHS
6351 let lhs_value = self.evaluate_internal(lhs, data)?;
6352 // Do regex match inline
6353 return match lhs_value {
6354 JValue::String(s) => {
6355 // Build the regex
6356 let case_insensitive = flags.contains('i');
6357 let regex_pattern = if case_insensitive {
6358 format!("(?i){}", pattern)
6359 } else {
6360 pattern.clone()
6361 };
6362 match regex::Regex::new(®ex_pattern) {
6363 Ok(re) => {
6364 if let Some(m) = re.find(&s) {
6365 // Return match object
6366 let mut result = IndexMap::new();
6367 result.insert(
6368 "match".to_string(),
6369 JValue::string(m.as_str().to_string()),
6370 );
6371 result.insert(
6372 "start".to_string(),
6373 JValue::Number(m.start() as f64),
6374 );
6375 result
6376 .insert("end".to_string(), JValue::Number(m.end() as f64));
6377
6378 // Capture groups
6379 let mut groups = Vec::new();
6380 for cap in re.captures_iter(&s).take(1) {
6381 for i in 1..cap.len() {
6382 if let Some(c) = cap.get(i) {
6383 groups.push(JValue::string(c.as_str().to_string()));
6384 }
6385 }
6386 }
6387 if !groups.is_empty() {
6388 result.insert("groups".to_string(), JValue::array(groups));
6389 }
6390
6391 Ok(JValue::object(result))
6392 } else {
6393 Ok(JValue::Null)
6394 }
6395 }
6396 Err(e) => Err(EvaluatorError::EvaluationError(format!(
6397 "Invalid regex: {}",
6398 e
6399 ))),
6400 }
6401 }
6402 JValue::Null => Ok(JValue::Null),
6403 _ => Err(EvaluatorError::TypeError(
6404 "Left side of ~> /regex/ must be a string".to_string(),
6405 )),
6406 };
6407 }
6408
6409 // Early check: if LHS evaluates to undefined, return undefined
6410 // This matches JSONata behavior where undefined ~> anyFunc returns undefined
6411 let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
6412 if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
6413 return Ok(JValue::Undefined);
6414 }
6415
6416 // Handle different RHS types
6417 match rhs {
6418 AstNode::Function {
6419 name,
6420 args,
6421 is_builtin,
6422 } => {
6423 // RHS is a function call
6424 // Check if the function call has placeholder arguments (partial application)
6425 let has_placeholder =
6426 args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6427
6428 if has_placeholder {
6429 // Partial application: replace the first placeholder with LHS value
6430 let lhs_value = self.evaluate_internal(lhs, data)?;
6431 let mut filled_args = Vec::new();
6432 let mut lhs_used = false;
6433
6434 for arg in args.iter() {
6435 if matches!(arg, AstNode::Placeholder) && !lhs_used {
6436 // Replace first placeholder with evaluated LHS
6437 // We need to create a temporary binding to pass the value
6438 let temp_name = format!("__pipe_arg_{}", filled_args.len());
6439 self.context.bind(temp_name.clone(), lhs_value.clone());
6440 filled_args.push(AstNode::Variable(temp_name));
6441 lhs_used = true;
6442 } else {
6443 filled_args.push(arg.clone());
6444 }
6445 }
6446
6447 // Evaluate the function with filled args
6448 let result =
6449 self.evaluate_function_call(name, &filled_args, *is_builtin, data);
6450
6451 // Clean up temp bindings
6452 for (i, arg) in args.iter().enumerate() {
6453 if matches!(arg, AstNode::Placeholder) {
6454 self.context.unbind(&format!("__pipe_arg_{}", i));
6455 }
6456 }
6457
6458 // Unwrap singleton results from chain operator
6459 return result.map(|v| self.unwrap_singleton(v));
6460 } else {
6461 // No placeholders: build args list with LHS as first argument
6462 let mut all_args = vec![lhs.clone()];
6463 all_args.extend_from_slice(args);
6464 // Unwrap singleton results from chain operator
6465 return self
6466 .evaluate_function_call(name, &all_args, *is_builtin, data)
6467 .map(|v| self.unwrap_singleton(v));
6468 }
6469 }
6470 AstNode::Variable(var_name) => {
6471 // RHS is a function reference (no parens)
6472 // e.g., $average($tempReadings) ~> $round
6473 let all_args = vec![lhs.clone()];
6474 // Unwrap singleton results from chain operator
6475 return self
6476 .evaluate_function_call(var_name, &all_args, true, data)
6477 .map(|v| self.unwrap_singleton(v));
6478 }
6479 AstNode::Binary {
6480 op: BinaryOp::ChainPipe,
6481 ..
6482 } => {
6483 // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
6484 // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
6485 let lhs_value = self.evaluate_internal(lhs, data)?;
6486 return self.evaluate_internal(rhs, &lhs_value);
6487 }
6488 AstNode::Transform { .. } => {
6489 // RHS is a transform - invoke it with LHS as input
6490 // Evaluate LHS first
6491 let lhs_value = self.evaluate_internal(lhs, data)?;
6492
6493 // Bind $ to the LHS value, then evaluate the transform
6494 let saved_binding = self.context.lookup("$").cloned();
6495 self.context.bind("$".to_string(), lhs_value.clone());
6496
6497 let result = self.evaluate_internal(rhs, data);
6498
6499 // Restore $ binding
6500 if let Some(saved) = saved_binding {
6501 self.context.bind("$".to_string(), saved);
6502 } else {
6503 self.context.unbind("$");
6504 }
6505
6506 // Unwrap singleton results from chain operator
6507 return result.map(|v| self.unwrap_singleton(v));
6508 }
6509 AstNode::Lambda {
6510 params,
6511 body,
6512 signature,
6513 thunk,
6514 } => {
6515 // RHS is a lambda - invoke it with LHS as argument
6516 let lhs_value = self.evaluate_internal(lhs, data)?;
6517 // Unwrap singleton results from chain operator
6518 return self
6519 .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
6520 .map(|v| self.unwrap_singleton(v));
6521 }
6522 AstNode::Path { steps } => {
6523 // RHS is a path expression (e.g., function call with predicate: $map($f)[])
6524 // If the first step is a function call, we need to add LHS as first argument
6525 if let Some(first_step) = steps.first() {
6526 match &first_step.node {
6527 AstNode::Function {
6528 name,
6529 args,
6530 is_builtin,
6531 } => {
6532 // Prepend LHS to the function arguments
6533 let mut all_args = vec![lhs.clone()];
6534 all_args.extend_from_slice(args);
6535
6536 // Call the function
6537 let mut result = self.evaluate_function_call(
6538 name,
6539 &all_args,
6540 *is_builtin,
6541 data,
6542 )?;
6543
6544 // Apply stages from the first step (e.g., predicates)
6545 for stage in &first_step.stages {
6546 match stage {
6547 Stage::Filter(filter_expr) => {
6548 result = self.evaluate_predicate_as_stage(
6549 &result,
6550 filter_expr,
6551 )?;
6552 }
6553 Stage::Index(_) => {}
6554 }
6555 }
6556
6557 // Apply remaining path steps if any
6558 if steps.len() > 1 {
6559 let remaining_path = AstNode::Path {
6560 steps: steps[1..].to_vec(),
6561 };
6562 result = self.evaluate_internal(&remaining_path, &result)?;
6563 }
6564
6565 // Unwrap singleton results from chain operator, unless there are stages
6566 // Stages (like predicates) indicate we want to preserve array structure
6567 if !first_step.stages.is_empty() || steps.len() > 1 {
6568 return Ok(result);
6569 } else {
6570 return Ok(self.unwrap_singleton(result));
6571 }
6572 }
6573 AstNode::Variable(var_name) => {
6574 // Variable that should resolve to a function
6575 let all_args = vec![lhs.clone()];
6576 let mut result =
6577 self.evaluate_function_call(var_name, &all_args, true, data)?;
6578
6579 // Apply stages from the first step
6580 for stage in &first_step.stages {
6581 match stage {
6582 Stage::Filter(filter_expr) => {
6583 result = self.evaluate_predicate_as_stage(
6584 &result,
6585 filter_expr,
6586 )?;
6587 }
6588 Stage::Index(_) => {}
6589 }
6590 }
6591
6592 // Apply remaining path steps if any
6593 if steps.len() > 1 {
6594 let remaining_path = AstNode::Path {
6595 steps: steps[1..].to_vec(),
6596 };
6597 result = self.evaluate_internal(&remaining_path, &result)?;
6598 }
6599
6600 // Unwrap singleton results from chain operator, unless there are stages
6601 // Stages (like predicates) indicate we want to preserve array structure
6602 if !first_step.stages.is_empty() || steps.len() > 1 {
6603 return Ok(result);
6604 } else {
6605 return Ok(self.unwrap_singleton(result));
6606 }
6607 }
6608 _ => {
6609 // Other path types - just evaluate normally with LHS as context
6610 let lhs_value = self.evaluate_internal(lhs, data)?;
6611 return self
6612 .evaluate_internal(rhs, &lhs_value)
6613 .map(|v| self.unwrap_singleton(v));
6614 }
6615 }
6616 }
6617
6618 // Empty path? Shouldn't happen, but handle it
6619 let lhs_value = self.evaluate_internal(lhs, data)?;
6620 return self
6621 .evaluate_internal(rhs, &lhs_value)
6622 .map(|v| self.unwrap_singleton(v));
6623 }
6624 _ => {
6625 return Err(EvaluatorError::TypeError(
6626 "Right side of ~> must be a function call or function reference"
6627 .to_string(),
6628 ));
6629 }
6630 }
6631 }
6632
6633 // Special handling for variable binding (:=)
6634 if op == BinaryOp::ColonEqual {
6635 // Extract variable name from LHS
6636 let var_name = match lhs {
6637 AstNode::Variable(name) => name.clone(),
6638 _ => {
6639 return Err(EvaluatorError::TypeError(
6640 "Left side of := must be a variable".to_string(),
6641 ))
6642 }
6643 };
6644
6645 // Check if RHS is a lambda - store it specially
6646 if let AstNode::Lambda {
6647 params,
6648 body,
6649 signature,
6650 thunk,
6651 } = rhs
6652 {
6653 // Store the lambda AST for later invocation
6654 // Capture only the free variables referenced by the lambda body
6655 let captured_env = self.capture_environment_for(body, params);
6656 let compiled_body = if !thunk {
6657 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
6658 try_compile_expr_with_allowed_vars(body, &var_refs)
6659 } else {
6660 None
6661 };
6662 let stored_lambda = StoredLambda {
6663 params: params.clone(),
6664 body: (**body).clone(),
6665 compiled_body,
6666 signature: signature.clone(),
6667 captured_env,
6668 captured_data: Some(data.clone()),
6669 thunk: *thunk,
6670 };
6671 let lambda_params = stored_lambda.params.clone();
6672 let lambda_sig = stored_lambda.signature.clone();
6673 self.context.bind_lambda(var_name.clone(), stored_lambda);
6674
6675 // Return a lambda marker value (include _lambda_id so it can be found later)
6676 let lambda_repr = JValue::lambda(
6677 var_name.as_str(),
6678 lambda_params,
6679 Some(var_name.clone()),
6680 lambda_sig,
6681 );
6682 return Ok(lambda_repr);
6683 }
6684
6685 // Check if RHS is a pure function composition (ChainPipe between function references)
6686 // e.g., $uppertrim := $trim ~> $uppercase
6687 // This creates a lambda that composes the functions.
6688 // But NOT for data ~> function, which should be evaluated immediately.
6689 // e.g., $result := data ~> $map($fn) should evaluate the pipe
6690 if let AstNode::Binary {
6691 op: BinaryOp::ChainPipe,
6692 lhs: chain_lhs,
6693 rhs: chain_rhs,
6694 } = rhs
6695 {
6696 // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
6697 // If LHS is data (array, object, function call result, etc.), evaluate the pipe
6698 let is_function_composition = match chain_lhs.as_ref() {
6699 // LHS is a function reference like $trim or $sum
6700 AstNode::Variable(name)
6701 if self.is_builtin_function(name)
6702 || self.context.lookup_lambda(name).is_some() =>
6703 {
6704 true
6705 }
6706 // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
6707 AstNode::Binary {
6708 op: BinaryOp::ChainPipe,
6709 ..
6710 } => true,
6711 // A function call with placeholder creates a partial application
6712 // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
6713 AstNode::Function { args, .. }
6714 if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
6715 {
6716 true
6717 }
6718 // Anything else (data, function calls, arrays, etc.) is not pure composition
6719 _ => false,
6720 };
6721
6722 if is_function_composition {
6723 // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
6724 // The original chain is $trim ~> $uppercase (left-associative)
6725 // We want to create: ($ ~> $trim) ~> $uppercase
6726 let param_name = "$".to_string();
6727
6728 // First create $ ~> $trim
6729 let first_pipe = AstNode::Binary {
6730 op: BinaryOp::ChainPipe,
6731 lhs: Box::new(AstNode::Variable(param_name.clone())),
6732 rhs: chain_lhs.clone(),
6733 };
6734
6735 // Then wrap with ~> $uppercase (or the rest of the chain)
6736 let composed_body = AstNode::Binary {
6737 op: BinaryOp::ChainPipe,
6738 lhs: Box::new(first_pipe),
6739 rhs: chain_rhs.clone(),
6740 };
6741
6742 let stored_lambda = StoredLambda {
6743 params: vec![param_name],
6744 body: composed_body,
6745 compiled_body: None, // ChainPipe body is not compilable
6746 signature: None,
6747 captured_env: self.capture_current_environment(),
6748 captured_data: Some(data.clone()),
6749 thunk: false,
6750 };
6751 self.context.bind_lambda(var_name.clone(), stored_lambda);
6752
6753 // Return a lambda marker value (include _lambda_id for later lookup)
6754 let lambda_repr = JValue::lambda(
6755 var_name.as_str(),
6756 vec!["$".to_string()],
6757 Some(var_name.clone()),
6758 None::<String>,
6759 );
6760 return Ok(lambda_repr);
6761 }
6762 // If not function composition, fall through to normal evaluation below
6763 }
6764
6765 // Evaluate the RHS
6766 let value = self.evaluate_internal(rhs, data)?;
6767
6768 // If the value is a lambda, copy the stored lambda to the new variable name
6769 if let Some(stored) = self.lookup_lambda_from_value(&value) {
6770 self.context.bind_lambda(var_name.clone(), stored);
6771 }
6772
6773 // Bind even if undefined (null) so inner scopes can shadow outer variables
6774 self.context.bind(var_name, value.clone());
6775 return Ok(value);
6776 }
6777
6778 // Special handling for 'In' operator - check for array filtering
6779 // Must evaluate lhs first to determine if this is array filtering
6780 if op == BinaryOp::In {
6781 let left = self.evaluate_internal(lhs, data)?;
6782
6783 // Check if this is array filtering: array[predicate]
6784 if matches!(left, JValue::Array(_)) {
6785 // Try evaluating rhs in current context to see if it's a simple index
6786 let right_result = self.evaluate_internal(rhs, data);
6787
6788 if let Ok(JValue::Number(_)) = right_result {
6789 // Simple numeric index: array[n]
6790 return self.array_index(&left, &right_result.unwrap());
6791 } else {
6792 // This is array filtering: array[predicate]
6793 // Evaluate the predicate for each array item
6794 return self.array_filter(lhs, rhs, &left, data);
6795 }
6796 }
6797 }
6798
6799 // Special handling for logical operators (short-circuit evaluation)
6800 if op == BinaryOp::And {
6801 let left = self.evaluate_internal(lhs, data)?;
6802 if !self.is_truthy(&left) {
6803 // Short-circuit: if left is falsy, return false without evaluating right
6804 return Ok(JValue::Bool(false));
6805 }
6806 let right = self.evaluate_internal(rhs, data)?;
6807 return Ok(JValue::Bool(self.is_truthy(&right)));
6808 }
6809
6810 if op == BinaryOp::Or {
6811 let left = self.evaluate_internal(lhs, data)?;
6812 if self.is_truthy(&left) {
6813 // Short-circuit: if left is truthy, return true without evaluating right
6814 return Ok(JValue::Bool(true));
6815 }
6816 let right = self.evaluate_internal(rhs, data)?;
6817 return Ok(JValue::Bool(self.is_truthy(&right)));
6818 }
6819
6820 // Check if operands are explicit null literals (vs undefined from variables)
6821 let left_is_explicit_null = matches!(lhs, AstNode::Null);
6822 let right_is_explicit_null = matches!(rhs, AstNode::Null);
6823
6824 // Standard evaluation: evaluate both operands
6825 let left = self.evaluate_internal(lhs, data)?;
6826 let right = self.evaluate_internal(rhs, data)?;
6827
6828 match op {
6829 BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6830 BinaryOp::Subtract => {
6831 self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6832 }
6833 BinaryOp::Multiply => {
6834 self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6835 }
6836 BinaryOp::Divide => {
6837 self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6838 }
6839 BinaryOp::Modulo => {
6840 self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6841 }
6842
6843 // compiled_equal normalizes lazy operands (guarded, zero-cost when neither
6844 // side is lazy) so conversion failures raise instead of silently comparing
6845 // unequal.
6846 BinaryOp::Equal => compiled_equal(&left, &right),
6847 BinaryOp::NotEqual => match compiled_equal(&left, &right)? {
6848 JValue::Bool(b) => Ok(JValue::Bool(!b)),
6849 other => Ok(other),
6850 },
6851 BinaryOp::LessThan => {
6852 self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6853 }
6854 BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6855 &left,
6856 &right,
6857 left_is_explicit_null,
6858 right_is_explicit_null,
6859 ),
6860 BinaryOp::GreaterThan => {
6861 self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6862 }
6863 BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6864 &left,
6865 &right,
6866 left_is_explicit_null,
6867 right_is_explicit_null,
6868 ),
6869
6870 // And/Or handled above with short-circuit evaluation
6871 BinaryOp::And | BinaryOp::Or => unreachable!(),
6872
6873 BinaryOp::Concatenate => self.concatenate(&left, &right),
6874 BinaryOp::Range => self.range(&left, &right),
6875 BinaryOp::In => self.in_operator(&left, &right),
6876
6877 // Focus binding: should be resolved by ast_transform pass (Task 2)
6878 BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6879 "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6880 )),
6881
6882 // Index binding: should be resolved by ast_transform pass (Task 4,
6883 // which retired the dedicated AstNode::IndexBind variant in favor
6884 // of this generic Binary marker, mirroring FocusBind above)
6885 BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6886 "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6887 )),
6888
6889 // These operators are all handled as special cases earlier in evaluate_binary_op
6890 BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6891 unreachable!()
6892 }
6893 }
6894 }
6895
6896 /// Evaluate a unary operation
6897 fn evaluate_unary_op(
6898 &mut self,
6899 op: crate::ast::UnaryOp,
6900 operand: &AstNode,
6901 data: &JValue,
6902 ) -> Result<JValue, EvaluatorError> {
6903 use crate::ast::UnaryOp;
6904
6905 let value = self.evaluate_internal(operand, data)?;
6906
6907 match op {
6908 UnaryOp::Negate => match value {
6909 // undefined returns undefined
6910 JValue::Null | JValue::Undefined => Ok(JValue::Null),
6911 JValue::Number(n) => Ok(JValue::Number(-n)),
6912 _ => Err(EvaluatorError::TypeError(
6913 "D1002: Cannot negate non-number value".to_string(),
6914 )),
6915 },
6916 UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6917 }
6918 }
6919
6920 /// Try to fuse an aggregate function call with its Path argument.
6921 /// Handles patterns like:
6922 /// - $sum(arr.field) → iterate arr, extract field, accumulate
6923 /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6924 ///
6925 /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6926 fn try_fused_aggregate(
6927 &mut self,
6928 name: &str,
6929 arg: &AstNode,
6930 data: &JValue,
6931 ) -> Result<Option<JValue>, EvaluatorError> {
6932 // Only applies to numeric aggregates
6933 if !matches!(name, "sum" | "max" | "min" | "average") {
6934 return Ok(None);
6935 }
6936
6937 // Argument must be a Path
6938 let AstNode::Path { steps } = arg else {
6939 return Ok(None);
6940 };
6941
6942 // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6943 // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6944 if steps.len() != 2 {
6945 return Ok(None);
6946 }
6947
6948 // Last step must be a simple Name (the field to extract)
6949 let field_step = &steps[1];
6950 if !field_step.stages.is_empty() {
6951 return Ok(None);
6952 }
6953 let AstNode::Name(extract_field) = &field_step.node else {
6954 return Ok(None);
6955 };
6956
6957 // First step: Name with optional filter stage
6958 let arr_step = &steps[0];
6959 let AstNode::Name(arr_name) = &arr_step.node else {
6960 return Ok(None);
6961 };
6962
6963 // Get the source array from data
6964 let arr = match data {
6965 JValue::Object(obj) => match obj.get(arr_name) {
6966 Some(JValue::Array(arr)) => arr,
6967 _ => return Ok(None),
6968 },
6969 _ => return Ok(None),
6970 };
6971
6972 // Check for filter stage — try CompiledExpr for the predicate
6973 let filter_compiled = match arr_step.stages.as_slice() {
6974 [] => None,
6975 [Stage::Filter(pred)] => try_compile_expr(pred),
6976 _ => return Ok(None),
6977 };
6978 // If filter stage exists but wasn't compilable, bail out
6979 if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6980 return Ok(None);
6981 }
6982
6983 // Build shape cache for the array
6984 let shape = arr.first().and_then(build_shape_cache);
6985
6986 // Fused iteration: filter (optional) + extract + aggregate
6987 let mut total = 0.0f64;
6988 let mut count = 0usize;
6989 let mut max_val = f64::NEG_INFINITY;
6990 let mut min_val = f64::INFINITY;
6991 let mut has_any = false;
6992
6993 for item in arr.iter() {
6994 // Apply compiled filter if present
6995 if let Some(ref compiled) = filter_compiled {
6996 let result = if let Some(ref s) = shape {
6997 eval_compiled_shaped(compiled, item, None, s, &self.options, self.start_time)?
6998 } else {
6999 eval_compiled(compiled, item, None, &self.options, self.start_time)?
7000 };
7001 if !compiled_is_truthy(&result) {
7002 continue;
7003 }
7004 }
7005
7006 // Extract field value
7007 let val = match item {
7008 JValue::Object(obj) => match obj.get(extract_field) {
7009 Some(JValue::Number(n)) => *n,
7010 Some(_) | None => continue, // Skip non-numeric / missing
7011 },
7012 _ => continue,
7013 };
7014
7015 has_any = true;
7016 match name {
7017 "sum" => total += val,
7018 "max" => max_val = max_val.max(val),
7019 "min" => min_val = min_val.min(val),
7020 "average" => {
7021 total += val;
7022 count += 1;
7023 }
7024 _ => unreachable!(),
7025 }
7026 }
7027
7028 if !has_any {
7029 return Ok(Some(match name {
7030 "sum" => JValue::from(0i64),
7031 "average" | "max" | "min" => JValue::Null,
7032 _ => unreachable!(),
7033 }));
7034 }
7035
7036 Ok(Some(match name {
7037 "sum" => JValue::Number(total),
7038 "max" => JValue::Number(max_val),
7039 "min" => JValue::Number(min_val),
7040 "average" => JValue::Number(total / count as f64),
7041 _ => unreachable!(),
7042 }))
7043 }
7044
7045 /// Evaluate a function call
7046 fn evaluate_function_call(
7047 &mut self,
7048 name: &str,
7049 args: &[AstNode],
7050 is_builtin: bool,
7051 data: &JValue,
7052 ) -> Result<JValue, EvaluatorError> {
7053 use crate::functions;
7054
7055 // Check for partial application (any argument is a Placeholder)
7056 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
7057 if has_placeholder {
7058 return self.create_partial_application(name, args, is_builtin, data);
7059 }
7060
7061 // FIRST check if this variable holds a function value (lambda or builtin reference)
7062 // This is critical for:
7063 // 1. Allowing function parameters to shadow stored lambdas
7064 // (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
7065 // 2. Calling built-in functions passed as parameters
7066 // (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
7067 if let Some(value) = self.context.lookup(name).cloned() {
7068 if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
7069 let mut evaluated_args = Vec::with_capacity(args.len());
7070 for arg in args {
7071 evaluated_args.push(self.evaluate_internal(arg, data)?);
7072 }
7073 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
7074 }
7075 if let JValue::Builtin { name: builtin_name } = &value {
7076 // This is a built-in function reference (e.g., $f bound to $sum)
7077 let mut evaluated_args = Vec::with_capacity(args.len());
7078 for arg in args {
7079 evaluated_args.push(self.evaluate_internal(arg, data)?);
7080 }
7081 return self.call_builtin_with_values(builtin_name, &evaluated_args);
7082 }
7083 }
7084
7085 // THEN check if this is a stored lambda (user-defined function by name)
7086 // This only applies if not shadowed by a binding above
7087 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
7088 let mut evaluated_args = Vec::with_capacity(args.len());
7089 for arg in args {
7090 evaluated_args.push(self.evaluate_internal(arg, data)?);
7091 }
7092 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
7093 }
7094
7095 // If the function was called without $ prefix and it's not a stored lambda,
7096 // it's an error (unknown function without $ prefix)
7097 if !is_builtin && name != "__lambda__" {
7098 return Err(EvaluatorError::ReferenceError(format!(
7099 "Unknown function: {}",
7100 name
7101 )));
7102 }
7103
7104 // Special handling for $exists function
7105 // It needs to know if the argument is explicit null vs undefined
7106 if name == "exists" && args.len() == 1 {
7107 let arg = &args[0];
7108
7109 // Check if it's an explicit null literal
7110 if matches!(arg, AstNode::Null) {
7111 return Ok(JValue::Bool(true)); // Explicit null exists
7112 }
7113
7114 // Check if it's a function reference
7115 if let AstNode::Variable(var_name) = arg {
7116 if self.is_builtin_function(var_name) {
7117 return Ok(JValue::Bool(true)); // Built-in function exists
7118 }
7119
7120 // Check if it's a stored lambda
7121 if self.context.lookup_lambda(var_name).is_some() {
7122 return Ok(JValue::Bool(true)); // Lambda exists
7123 }
7124
7125 // Check if the variable is defined
7126 if let Some(val) = self.context.lookup(var_name) {
7127 // A variable bound to the undefined marker doesn't "exist"
7128 if val.is_undefined() {
7129 return Ok(JValue::Bool(false));
7130 }
7131 return Ok(JValue::Bool(true)); // Variable is defined (even if null)
7132 } else {
7133 return Ok(JValue::Bool(false)); // Variable is undefined
7134 }
7135 }
7136
7137 // For other expressions, evaluate and check if non-null/non-undefined
7138 let value = self.evaluate_internal(arg, data)?;
7139 return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
7140 }
7141
7142 // Check if any arguments are undefined variables or undefined paths
7143 // Functions like $not() should return undefined when given undefined values
7144 for arg in args {
7145 // Check for undefined variable (e.g., $undefined_var)
7146 if let AstNode::Variable(var_name) = arg {
7147 // Skip built-in function names - they're function references, not undefined variables
7148 if !var_name.is_empty()
7149 && !self.is_builtin_function(var_name)
7150 && self.context.lookup(var_name).is_none()
7151 {
7152 // Undefined variable - for functions that should propagate undefined
7153 if propagates_undefined(name) {
7154 return Ok(JValue::Null); // Return undefined
7155 }
7156 }
7157 }
7158 // Check for simple field name (e.g., blah) that evaluates to undefined
7159 if let AstNode::Name(field_name) = arg {
7160 let field_exists = matches!(data, JValue::Object(obj) if obj.contains_key(field_name))
7161 || {
7162 #[cfg(feature = "python")]
7163 {
7164 matches!(data, JValue::LazyPyDict(l) if l.contains_field(field_name))
7165 }
7166 #[cfg(not(feature = "python"))]
7167 {
7168 false
7169 }
7170 };
7171 if !field_exists && propagates_undefined(name) {
7172 return Ok(JValue::Null);
7173 }
7174 }
7175 // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
7176 // Field accesses are represented as AstNode::Path. String literals should never
7177 // be checked for undefined propagation.
7178 // Check for Path expressions that evaluate to undefined
7179 if let AstNode::Path { steps } = arg {
7180 // For paths that evaluate to null, we need to determine if it's because:
7181 // 1. A field doesn't exist (undefined) - should propagate as undefined
7182 // 2. A field exists with value null - should throw T0410
7183 //
7184 // We can distinguish these by checking if the path is accessing a field
7185 // that doesn't exist on an object vs one that has an explicit null value.
7186 if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
7187 // Path evaluated to null - now check if it's truly undefined
7188 // For single-step paths, check if the field exists
7189 if steps.len() == 1 {
7190 // Get field name - could be Name (identifier) or String (quoted)
7191 let field_name = match &steps[0].node {
7192 AstNode::Name(n) => Some(n.as_str()),
7193 AstNode::String(s) => Some(s.as_str()),
7194 _ => None,
7195 };
7196 if let Some(field) = field_name {
7197 match data {
7198 JValue::Object(obj) => {
7199 if !obj.contains_key(field) {
7200 // Field doesn't exist - return undefined
7201 if propagates_undefined(name) {
7202 return Ok(JValue::Null);
7203 }
7204 }
7205 // Field exists with value null - continue to throw T0410
7206 }
7207 // Trying to access field on null data - return undefined
7208 JValue::Null if propagates_undefined(name) => {
7209 return Ok(JValue::Null);
7210 }
7211 _ => {}
7212 }
7213 }
7214 }
7215 // For multi-step paths, check if any intermediate step failed
7216 else if steps.len() > 1 {
7217 // Evaluate each step to find where it breaks
7218 let mut current = data;
7219 let mut failed_due_to_missing_field = false;
7220
7221 for (i, step) in steps.iter().enumerate() {
7222 if let AstNode::Name(field_name) = &step.node {
7223 match current {
7224 JValue::Object(obj) => {
7225 if let Some(val) = obj.get(field_name) {
7226 current = val;
7227 } else {
7228 // Field doesn't exist
7229 failed_due_to_missing_field = true;
7230 break;
7231 }
7232 }
7233 JValue::Array(_) => {
7234 // Array access - evaluate normally
7235 break;
7236 }
7237 JValue::Null => {
7238 // Hit null in the middle of the path
7239 if i > 0 {
7240 // Previous field had null value - not undefined
7241 failed_due_to_missing_field = false;
7242 }
7243 break;
7244 }
7245 _ => break,
7246 }
7247 }
7248 }
7249
7250 if failed_due_to_missing_field && propagates_undefined(name) {
7251 return Ok(JValue::Null);
7252 }
7253 }
7254 }
7255 }
7256 }
7257
7258 // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
7259 // try to fuse filter+extract+aggregate into a single pass.
7260 if args.len() == 1 {
7261 if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
7262 return Ok(result);
7263 }
7264 }
7265
7266 let mut evaluated_args = Vec::with_capacity(args.len());
7267 for arg in args {
7268 evaluated_args.push(self.evaluate_internal(arg, data)?);
7269 }
7270
7271 // JSONata feature: when a function is called with no arguments but expects
7272 // at least one, use the current context value (data) as the implicit first argument
7273 // This also applies when functions expecting N arguments receive N-1 arguments,
7274 // in which case the context value becomes the first argument
7275 let context_functions_zero_arg = [
7276 "string",
7277 "number",
7278 "boolean",
7279 "uppercase",
7280 "lowercase",
7281 "fromMillis",
7282 ];
7283 let context_functions_missing_first = [
7284 "substringBefore",
7285 "substringAfter",
7286 "contains",
7287 "split",
7288 "replace",
7289 ];
7290
7291 if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
7292 // Use the current context value as the implicit argument
7293 evaluated_args.push(data.clone());
7294 } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
7295 // These functions expect 2+ arguments, but received 1
7296 // Only insert context if it's a compatible type (string for string functions)
7297 // Otherwise, let the function throw T0411 for wrong argument count
7298 if matches!(data, JValue::String(_)) {
7299 evaluated_args.insert(0, data.clone());
7300 }
7301 }
7302
7303 // Special handling for $string() with no explicit arguments
7304 // After context insertion, check if the argument is null (undefined context)
7305 if name == "string"
7306 && args.is_empty()
7307 && !evaluated_args.is_empty()
7308 && evaluated_args[0].is_null()
7309 {
7310 // Context was null/undefined, so return undefined
7311 return Ok(JValue::Null);
7312 }
7313
7314 #[cfg(feature = "python")]
7315 for arg in evaluated_args.iter_mut() {
7316 if matches!(arg, JValue::LazyPyDict(_)) {
7317 *arg = normalize_lazy(arg)?;
7318 }
7319 }
7320
7321 match name {
7322 "string" => {
7323 if evaluated_args.len() > 2 {
7324 return Err(EvaluatorError::EvaluationError(
7325 "string() takes at most 2 arguments".to_string(),
7326 ));
7327 }
7328
7329 let prettify = if evaluated_args.len() == 2 {
7330 match &evaluated_args[1] {
7331 JValue::Bool(b) => Some(*b),
7332 _ => {
7333 return Err(EvaluatorError::TypeError(
7334 "string() prettify parameter must be a boolean".to_string(),
7335 ))
7336 }
7337 }
7338 } else {
7339 None
7340 };
7341
7342 Ok(functions::string::string(&evaluated_args[0], prettify)?)
7343 }
7344 "length" => {
7345 if evaluated_args.len() != 1 {
7346 return Err(EvaluatorError::EvaluationError(
7347 "length() requires exactly 1 argument".to_string(),
7348 ));
7349 }
7350 if evaluated_args[0].is_undefined() {
7351 return Ok(JValue::Undefined);
7352 }
7353 match &evaluated_args[0] {
7354 JValue::String(s) => Ok(functions::string::length(s)?),
7355 _ => Err(EvaluatorError::TypeError(
7356 "T0410: Argument 1 of function length does not match function signature"
7357 .to_string(),
7358 )),
7359 }
7360 }
7361 "uppercase" => {
7362 if evaluated_args.len() != 1 {
7363 return Err(EvaluatorError::EvaluationError(
7364 "uppercase() requires exactly 1 argument".to_string(),
7365 ));
7366 }
7367 if evaluated_args[0].is_undefined() {
7368 return Ok(JValue::Undefined);
7369 }
7370 match &evaluated_args[0] {
7371 JValue::String(s) => Ok(functions::string::uppercase(s)?),
7372 _ => Err(EvaluatorError::TypeError(
7373 "T0410: Argument 1 of function uppercase does not match function signature"
7374 .to_string(),
7375 )),
7376 }
7377 }
7378 "lowercase" => {
7379 if evaluated_args.len() != 1 {
7380 return Err(EvaluatorError::EvaluationError(
7381 "lowercase() requires exactly 1 argument".to_string(),
7382 ));
7383 }
7384 if evaluated_args[0].is_undefined() {
7385 return Ok(JValue::Undefined);
7386 }
7387 match &evaluated_args[0] {
7388 JValue::String(s) => Ok(functions::string::lowercase(s)?),
7389 _ => Err(EvaluatorError::TypeError(
7390 "T0410: Argument 1 of function lowercase does not match function signature"
7391 .to_string(),
7392 )),
7393 }
7394 }
7395 "number" => {
7396 if evaluated_args.is_empty() {
7397 return Err(EvaluatorError::EvaluationError(
7398 "number() requires at least 1 argument".to_string(),
7399 ));
7400 }
7401 if evaluated_args.len() > 1 {
7402 return Err(EvaluatorError::TypeError(
7403 "T0410: Argument 2 of function number does not match function signature"
7404 .to_string(),
7405 ));
7406 }
7407 if evaluated_args[0].is_undefined() {
7408 return Ok(JValue::Undefined);
7409 }
7410 Ok(functions::numeric::number(&evaluated_args[0])?)
7411 }
7412 "sum" => {
7413 if evaluated_args.len() != 1 {
7414 return Err(EvaluatorError::EvaluationError(
7415 "sum() requires exactly 1 argument".to_string(),
7416 ));
7417 }
7418 // Return undefined if argument is undefined
7419 if evaluated_args[0].is_undefined() {
7420 return Ok(JValue::Undefined);
7421 }
7422 match &evaluated_args[0] {
7423 JValue::Null => Ok(JValue::Null),
7424 JValue::Array(arr) => {
7425 // Use zero-clone iterator-based sum
7426 Ok(aggregation::sum(arr)?)
7427 }
7428 // Non-array values: extract number directly
7429 JValue::Number(n) => Ok(JValue::Number(*n)),
7430 other => Ok(functions::numeric::sum(&[other.clone()])?),
7431 }
7432 }
7433 "count" => {
7434 if evaluated_args.len() != 1 {
7435 return Err(EvaluatorError::EvaluationError(
7436 "count() requires exactly 1 argument".to_string(),
7437 ));
7438 }
7439 // Return 0 if argument is undefined
7440 if evaluated_args[0].is_undefined() {
7441 return Ok(JValue::from(0i64));
7442 }
7443 match &evaluated_args[0] {
7444 JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
7445 JValue::Array(arr) => Ok(functions::array::count(arr)?),
7446 _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
7447 }
7448 }
7449 "substring" => {
7450 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7451 return Err(EvaluatorError::EvaluationError(
7452 "substring() requires 2 or 3 arguments".to_string(),
7453 ));
7454 }
7455 if evaluated_args[0].is_undefined() {
7456 return Ok(JValue::Undefined);
7457 }
7458 match (&evaluated_args[0], &evaluated_args[1]) {
7459 (JValue::String(s), JValue::Number(start)) => {
7460 let length = if evaluated_args.len() == 3 {
7461 match &evaluated_args[2] {
7462 JValue::Number(l) => Some(*l as i64),
7463 _ => return Err(EvaluatorError::TypeError(
7464 "T0410: Argument 3 of function substring does not match function signature".to_string(),
7465 )),
7466 }
7467 } else {
7468 None
7469 };
7470 Ok(functions::string::substring(s, *start as i64, length)?)
7471 }
7472 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7473 "T0410: Argument 2 of function substring does not match function signature"
7474 .to_string(),
7475 )),
7476 _ => Err(EvaluatorError::TypeError(
7477 "T0410: Argument 1 of function substring does not match function signature"
7478 .to_string(),
7479 )),
7480 }
7481 }
7482 "substringBefore" => {
7483 if evaluated_args.len() != 2 {
7484 return Err(EvaluatorError::TypeError(
7485 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
7486 ));
7487 }
7488 if evaluated_args[0].is_undefined() {
7489 return Ok(JValue::Undefined);
7490 }
7491 match (&evaluated_args[0], &evaluated_args[1]) {
7492 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
7493 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7494 "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
7495 )),
7496 _ => Err(EvaluatorError::TypeError(
7497 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
7498 )),
7499 }
7500 }
7501 "substringAfter" => {
7502 if evaluated_args.len() != 2 {
7503 return Err(EvaluatorError::TypeError(
7504 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
7505 ));
7506 }
7507 if evaluated_args[0].is_undefined() {
7508 return Ok(JValue::Undefined);
7509 }
7510 match (&evaluated_args[0], &evaluated_args[1]) {
7511 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
7512 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
7513 "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
7514 )),
7515 _ => Err(EvaluatorError::TypeError(
7516 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
7517 )),
7518 }
7519 }
7520 "pad" => {
7521 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7522 return Err(EvaluatorError::EvaluationError(
7523 "pad() requires 2 or 3 arguments".to_string(),
7524 ));
7525 }
7526
7527 // First argument: string to pad
7528 let string = match &evaluated_args[0] {
7529 JValue::String(s) => s.clone(),
7530 JValue::Null => return Ok(JValue::Null),
7531 JValue::Undefined => return Ok(JValue::Undefined),
7532 _ => {
7533 return Err(EvaluatorError::TypeError(
7534 "pad() first argument must be a string".to_string(),
7535 ))
7536 }
7537 };
7538
7539 // Second argument: width (negative = left pad, positive = right pad)
7540 let width = match &evaluated_args.get(1) {
7541 Some(JValue::Number(n)) => *n as i32,
7542 _ => {
7543 return Err(EvaluatorError::TypeError(
7544 "pad() second argument must be a number".to_string(),
7545 ))
7546 }
7547 };
7548
7549 // Third argument: padding string (optional, defaults to space)
7550 let pad_string = match evaluated_args.get(2) {
7551 Some(JValue::String(s)) if !s.is_empty() => s.clone(),
7552 _ => Rc::from(" "),
7553 };
7554
7555 let abs_width = width.unsigned_abs() as usize;
7556 // Count Unicode characters (code points), not bytes
7557 let char_count = string.chars().count();
7558
7559 if char_count >= abs_width {
7560 // String is already long enough
7561 return Ok(JValue::string(string));
7562 }
7563
7564 let padding_needed = abs_width - char_count;
7565
7566 let pad_chars: Vec<char> = pad_string.chars().collect();
7567 let mut padding = String::with_capacity(padding_needed);
7568 for i in 0..padding_needed {
7569 padding.push(pad_chars[i % pad_chars.len()]);
7570 }
7571
7572 let result = if width < 0 {
7573 // Left pad (negative width)
7574 format!("{}{}", padding, string)
7575 } else {
7576 // Right pad (positive width)
7577 format!("{}{}", string, padding)
7578 };
7579
7580 Ok(JValue::string(result))
7581 }
7582
7583 "trim" => {
7584 if evaluated_args.is_empty() {
7585 return Ok(JValue::Null); // undefined
7586 }
7587 if evaluated_args.len() != 1 {
7588 return Err(EvaluatorError::EvaluationError(
7589 "trim() requires at most 1 argument".to_string(),
7590 ));
7591 }
7592 match &evaluated_args[0] {
7593 JValue::Null => Ok(JValue::Null),
7594 JValue::String(s) => Ok(functions::string::trim(s)?),
7595 _ => Err(EvaluatorError::TypeError(
7596 "trim() requires a string argument".to_string(),
7597 )),
7598 }
7599 }
7600 "contains" => {
7601 if evaluated_args.len() != 2 {
7602 return Err(EvaluatorError::EvaluationError(
7603 "contains() requires exactly 2 arguments".to_string(),
7604 ));
7605 }
7606 if evaluated_args[0].is_null() {
7607 return Ok(JValue::Null);
7608 }
7609 if evaluated_args[0].is_undefined() {
7610 return Ok(JValue::Undefined);
7611 }
7612 match &evaluated_args[0] {
7613 JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
7614 _ => Err(EvaluatorError::TypeError(
7615 "contains() requires a string as the first argument".to_string(),
7616 )),
7617 }
7618 }
7619 "split" => {
7620 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7621 return Err(EvaluatorError::EvaluationError(
7622 "split() requires 2 or 3 arguments".to_string(),
7623 ));
7624 }
7625 if evaluated_args[0].is_null() {
7626 return Ok(JValue::Null);
7627 }
7628 if evaluated_args[0].is_undefined() {
7629 return Ok(JValue::Undefined);
7630 }
7631 match &evaluated_args[0] {
7632 JValue::String(s) => {
7633 let limit = if evaluated_args.len() == 3 {
7634 match &evaluated_args[2] {
7635 JValue::Number(n) => {
7636 let f = *n;
7637 // Negative limit is an error
7638 if f < 0.0 {
7639 return Err(EvaluatorError::EvaluationError(
7640 "D3020: Third argument of split function must be a positive number".to_string(),
7641 ));
7642 }
7643 // Floor the value for non-integer limits
7644 Some(f.floor() as usize)
7645 }
7646 _ => {
7647 return Err(EvaluatorError::TypeError(
7648 "split() limit must be a number".to_string(),
7649 ))
7650 }
7651 }
7652 } else {
7653 None
7654 };
7655 Ok(functions::string::split(s, &evaluated_args[1], limit)?)
7656 }
7657 _ => Err(EvaluatorError::TypeError(
7658 "split() requires a string as the first argument".to_string(),
7659 )),
7660 }
7661 }
7662 "join" => {
7663 // Special case: if first arg is undefined, return undefined
7664 // But if separator (2nd arg) is undefined, use empty string (default)
7665 if evaluated_args.is_empty() {
7666 return Err(EvaluatorError::TypeError(
7667 "T0410: Argument 1 of function $join does not match function signature"
7668 .to_string(),
7669 ));
7670 }
7671 if evaluated_args[0].is_null() {
7672 return Ok(JValue::Null);
7673 }
7674 if evaluated_args[0].is_undefined() {
7675 return Ok(JValue::Undefined);
7676 }
7677
7678 // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
7679 // The signature handles coercion and validation
7680 use crate::signature::Signature;
7681
7682 let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
7683 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7684 })?;
7685
7686 let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
7687 Ok(args) => args,
7688 Err(crate::signature::SignatureError::UndefinedArgument) => {
7689 // This can happen if the separator is undefined
7690 // In that case, just validate the first arg and use default separator
7691 let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
7692 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
7693 })?;
7694
7695 match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
7696 Ok(args) => args,
7697 Err(crate::signature::SignatureError::ArrayTypeMismatch {
7698 index,
7699 expected,
7700 }) => {
7701 return Err(EvaluatorError::TypeError(format!(
7702 "T0412: Argument {} of function $join must be an array of {}",
7703 index, expected
7704 )));
7705 }
7706 Err(e) => {
7707 return Err(EvaluatorError::TypeError(format!(
7708 "Signature validation failed: {}",
7709 e
7710 )));
7711 }
7712 }
7713 }
7714 Err(crate::signature::SignatureError::ArgumentTypeMismatch {
7715 index,
7716 expected,
7717 }) => {
7718 return Err(EvaluatorError::TypeError(
7719 format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
7720 ));
7721 }
7722 Err(crate::signature::SignatureError::ArrayTypeMismatch {
7723 index,
7724 expected,
7725 }) => {
7726 return Err(EvaluatorError::TypeError(format!(
7727 "T0412: Argument {} of function $join must be an array of {}",
7728 index, expected
7729 )));
7730 }
7731 Err(e) => {
7732 return Err(EvaluatorError::TypeError(format!(
7733 "Signature validation failed: {}",
7734 e
7735 )));
7736 }
7737 };
7738
7739 // After coercion, first arg is guaranteed to be an array of strings
7740 match &coerced_args[0] {
7741 JValue::Array(arr) => {
7742 let separator = if coerced_args.len() == 2 {
7743 match &coerced_args[1] {
7744 JValue::String(s) => Some(&**s),
7745 JValue::Null => None, // Undefined separator -> use empty string
7746 _ => None, // Signature should have validated this
7747 }
7748 } else {
7749 None // No separator provided -> use empty string
7750 };
7751 Ok(functions::string::join(arr, separator)?)
7752 }
7753 JValue::Null => Ok(JValue::Null),
7754 _ => unreachable!("Signature validation should ensure array type"),
7755 }
7756 }
7757 "replace" => {
7758 if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
7759 return Err(EvaluatorError::EvaluationError(
7760 "replace() requires 3 or 4 arguments".to_string(),
7761 ));
7762 }
7763 if evaluated_args[0].is_null() {
7764 return Ok(JValue::Null);
7765 }
7766 if evaluated_args[0].is_undefined() {
7767 return Ok(JValue::Undefined);
7768 }
7769
7770 // Check if replacement (3rd arg) is a function/lambda
7771 let replacement_is_lambda = matches!(
7772 evaluated_args[2],
7773 JValue::Lambda { .. } | JValue::Builtin { .. }
7774 );
7775
7776 if replacement_is_lambda {
7777 // Lambda replacement mode
7778 return self.replace_with_lambda(
7779 &evaluated_args[0],
7780 &evaluated_args[1],
7781 &evaluated_args[2],
7782 if evaluated_args.len() == 4 {
7783 Some(&evaluated_args[3])
7784 } else {
7785 None
7786 },
7787 data,
7788 );
7789 }
7790
7791 // String replacement mode
7792 match (&evaluated_args[0], &evaluated_args[2]) {
7793 (JValue::String(s), JValue::String(replacement)) => {
7794 let limit = if evaluated_args.len() == 4 {
7795 match &evaluated_args[3] {
7796 JValue::Number(n) => {
7797 let lim_f64 = *n;
7798 if lim_f64 < 0.0 {
7799 return Err(EvaluatorError::EvaluationError(format!(
7800 "D3011: Limit must be non-negative, got {}",
7801 lim_f64
7802 )));
7803 }
7804 Some(lim_f64 as usize)
7805 }
7806 _ => {
7807 return Err(EvaluatorError::TypeError(
7808 "replace() limit must be a number".to_string(),
7809 ))
7810 }
7811 }
7812 } else {
7813 None
7814 };
7815 Ok(functions::string::replace(
7816 s,
7817 &evaluated_args[1],
7818 replacement,
7819 limit,
7820 )?)
7821 }
7822 _ => Err(EvaluatorError::TypeError(
7823 "replace() requires string arguments".to_string(),
7824 )),
7825 }
7826 }
7827 "match" => {
7828 // $match(str, pattern [, limit])
7829 // Returns array of match objects for regex matches or custom matcher function
7830 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7831 return Err(EvaluatorError::EvaluationError(
7832 "match() requires 1 to 3 arguments".to_string(),
7833 ));
7834 }
7835 if evaluated_args[0].is_null() {
7836 return Ok(JValue::Null);
7837 }
7838 if evaluated_args[0].is_undefined() {
7839 return Ok(JValue::Undefined);
7840 }
7841
7842 let s = match &evaluated_args[0] {
7843 JValue::String(s) => s.clone(),
7844 _ => {
7845 return Err(EvaluatorError::TypeError(
7846 "match() first argument must be a string".to_string(),
7847 ))
7848 }
7849 };
7850
7851 // Get optional limit
7852 let limit = if evaluated_args.len() == 3 {
7853 match &evaluated_args[2] {
7854 JValue::Number(n) => Some(*n as usize),
7855 JValue::Null => None,
7856 _ => {
7857 return Err(EvaluatorError::TypeError(
7858 "match() limit must be a number".to_string(),
7859 ))
7860 }
7861 }
7862 } else {
7863 None
7864 };
7865
7866 // Check if second argument is a custom matcher function (lambda)
7867 let pattern_value = evaluated_args.get(1);
7868 let is_custom_matcher = pattern_value.is_some_and(|val| {
7869 matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7870 });
7871
7872 if is_custom_matcher {
7873 // Custom matcher function support
7874 // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7875 return self.match_with_custom_matcher(&s, &args[1], limit, data);
7876 }
7877
7878 // Get regex pattern from second argument
7879 let (pattern, flags) = match pattern_value {
7880 Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7881 EvaluatorError::TypeError(
7882 "match() second argument must be a regex pattern or matcher function"
7883 .to_string(),
7884 )
7885 })?,
7886 None => (".*".to_string(), "".to_string()),
7887 };
7888
7889 // Build regex
7890 let is_global = flags.contains('g');
7891 let regex_pattern = if flags.contains('i') {
7892 format!("(?i){}", pattern)
7893 } else {
7894 pattern.clone()
7895 };
7896
7897 let re = regex::Regex::new(®ex_pattern).map_err(|e| {
7898 EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7899 })?;
7900
7901 let mut results = Vec::new();
7902 let mut count = 0;
7903
7904 for caps in re.captures_iter(&s) {
7905 if let Some(lim) = limit {
7906 if count >= lim {
7907 break;
7908 }
7909 }
7910
7911 let full_match = caps.get(0).unwrap();
7912 let mut match_obj = IndexMap::new();
7913 match_obj.insert(
7914 "match".to_string(),
7915 JValue::string(full_match.as_str().to_string()),
7916 );
7917 match_obj.insert(
7918 "index".to_string(),
7919 JValue::Number(full_match.start() as f64),
7920 );
7921
7922 // Collect capture groups
7923 let mut groups: Vec<JValue> = Vec::new();
7924 for i in 1..caps.len() {
7925 if let Some(group) = caps.get(i) {
7926 groups.push(JValue::string(group.as_str().to_string()));
7927 } else {
7928 groups.push(JValue::Null);
7929 }
7930 }
7931 if !groups.is_empty() {
7932 match_obj.insert("groups".to_string(), JValue::array(groups));
7933 }
7934
7935 results.push(JValue::object(match_obj));
7936 count += 1;
7937
7938 // If not global, only return first match
7939 if !is_global {
7940 break;
7941 }
7942 }
7943
7944 if results.is_empty() {
7945 Ok(JValue::Null)
7946 } else if results.len() == 1 && !is_global {
7947 // Single match (non-global) returns the match object directly
7948 Ok(results.into_iter().next().unwrap())
7949 } else {
7950 Ok(JValue::array(results))
7951 }
7952 }
7953 "max" => {
7954 if evaluated_args.len() != 1 {
7955 return Err(EvaluatorError::EvaluationError(
7956 "max() requires exactly 1 argument".to_string(),
7957 ));
7958 }
7959 // Check for undefined
7960 if evaluated_args[0].is_undefined() {
7961 return Ok(JValue::Undefined);
7962 }
7963 match &evaluated_args[0] {
7964 JValue::Null => Ok(JValue::Null),
7965 JValue::Array(arr) => {
7966 // Use zero-clone iterator-based max
7967 Ok(aggregation::max(arr)?)
7968 }
7969 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7970 _ => Err(EvaluatorError::TypeError(
7971 "max() requires an array or number argument".to_string(),
7972 )),
7973 }
7974 }
7975 "min" => {
7976 if evaluated_args.len() != 1 {
7977 return Err(EvaluatorError::EvaluationError(
7978 "min() requires exactly 1 argument".to_string(),
7979 ));
7980 }
7981 // Check for undefined
7982 if evaluated_args[0].is_undefined() {
7983 return Ok(JValue::Undefined);
7984 }
7985 match &evaluated_args[0] {
7986 JValue::Null => Ok(JValue::Null),
7987 JValue::Array(arr) => {
7988 // Use zero-clone iterator-based min
7989 Ok(aggregation::min(arr)?)
7990 }
7991 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7992 _ => Err(EvaluatorError::TypeError(
7993 "min() requires an array or number argument".to_string(),
7994 )),
7995 }
7996 }
7997 "average" => {
7998 if evaluated_args.len() != 1 {
7999 return Err(EvaluatorError::EvaluationError(
8000 "average() requires exactly 1 argument".to_string(),
8001 ));
8002 }
8003 // Return undefined if argument is undefined
8004 if evaluated_args[0].is_undefined() {
8005 return Ok(JValue::Undefined);
8006 }
8007 match &evaluated_args[0] {
8008 JValue::Null => Ok(JValue::Null),
8009 JValue::Array(arr) => {
8010 // Use zero-clone iterator-based average
8011 Ok(aggregation::average(arr)?)
8012 }
8013 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
8014 _ => Err(EvaluatorError::TypeError(
8015 "average() requires an array or number argument".to_string(),
8016 )),
8017 }
8018 }
8019 "abs" => {
8020 if evaluated_args.len() != 1 {
8021 return Err(EvaluatorError::EvaluationError(
8022 "abs() requires exactly 1 argument".to_string(),
8023 ));
8024 }
8025 if evaluated_args[0].is_undefined() {
8026 return Ok(JValue::Undefined);
8027 }
8028 match &evaluated_args[0] {
8029 JValue::Null => Ok(JValue::Null),
8030 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
8031 _ => Err(EvaluatorError::TypeError(
8032 "abs() requires a number argument".to_string(),
8033 )),
8034 }
8035 }
8036 "floor" => {
8037 if evaluated_args.len() != 1 {
8038 return Err(EvaluatorError::EvaluationError(
8039 "floor() requires exactly 1 argument".to_string(),
8040 ));
8041 }
8042 if evaluated_args[0].is_undefined() {
8043 return Ok(JValue::Undefined);
8044 }
8045 match &evaluated_args[0] {
8046 JValue::Null => Ok(JValue::Null),
8047 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
8048 _ => Err(EvaluatorError::TypeError(
8049 "floor() requires a number argument".to_string(),
8050 )),
8051 }
8052 }
8053 "ceil" => {
8054 if evaluated_args.len() != 1 {
8055 return Err(EvaluatorError::EvaluationError(
8056 "ceil() requires exactly 1 argument".to_string(),
8057 ));
8058 }
8059 if evaluated_args[0].is_undefined() {
8060 return Ok(JValue::Undefined);
8061 }
8062 match &evaluated_args[0] {
8063 JValue::Null => Ok(JValue::Null),
8064 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
8065 _ => Err(EvaluatorError::TypeError(
8066 "ceil() requires a number argument".to_string(),
8067 )),
8068 }
8069 }
8070 "round" => {
8071 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8072 return Err(EvaluatorError::EvaluationError(
8073 "round() requires 1 or 2 arguments".to_string(),
8074 ));
8075 }
8076 if evaluated_args[0].is_undefined() {
8077 return Ok(JValue::Undefined);
8078 }
8079 match &evaluated_args[0] {
8080 JValue::Null => Ok(JValue::Null),
8081 JValue::Number(n) => {
8082 let precision = if evaluated_args.len() == 2 {
8083 match &evaluated_args[1] {
8084 JValue::Number(p) => Some(*p as i32),
8085 _ => {
8086 return Err(EvaluatorError::TypeError(
8087 "round() precision must be a number".to_string(),
8088 ))
8089 }
8090 }
8091 } else {
8092 None
8093 };
8094 Ok(functions::numeric::round(*n, precision)?)
8095 }
8096 _ => Err(EvaluatorError::TypeError(
8097 "round() requires a number argument".to_string(),
8098 )),
8099 }
8100 }
8101 "sqrt" => {
8102 if evaluated_args.len() != 1 {
8103 return Err(EvaluatorError::EvaluationError(
8104 "sqrt() requires exactly 1 argument".to_string(),
8105 ));
8106 }
8107 if evaluated_args[0].is_undefined() {
8108 return Ok(JValue::Undefined);
8109 }
8110 match &evaluated_args[0] {
8111 JValue::Null => Ok(JValue::Null),
8112 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
8113 _ => Err(EvaluatorError::TypeError(
8114 "sqrt() requires a number argument".to_string(),
8115 )),
8116 }
8117 }
8118 "power" => {
8119 if evaluated_args.len() != 2 {
8120 return Err(EvaluatorError::EvaluationError(
8121 "power() requires exactly 2 arguments".to_string(),
8122 ));
8123 }
8124 if evaluated_args[0].is_null() {
8125 return Ok(JValue::Null);
8126 }
8127 if evaluated_args[0].is_undefined() {
8128 return Ok(JValue::Undefined);
8129 }
8130 match (&evaluated_args[0], &evaluated_args[1]) {
8131 (JValue::Number(base), JValue::Number(exp)) => {
8132 Ok(functions::numeric::power(*base, *exp)?)
8133 }
8134 _ => Err(EvaluatorError::TypeError(
8135 "power() requires number arguments".to_string(),
8136 )),
8137 }
8138 }
8139 "formatNumber" => {
8140 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
8141 return Err(EvaluatorError::EvaluationError(
8142 "formatNumber() requires 2 or 3 arguments".to_string(),
8143 ));
8144 }
8145 if evaluated_args[0].is_null() {
8146 return Ok(JValue::Null);
8147 }
8148 if evaluated_args[0].is_undefined() {
8149 return Ok(JValue::Undefined);
8150 }
8151 match (&evaluated_args[0], &evaluated_args[1]) {
8152 (JValue::Number(num), JValue::String(picture)) => {
8153 let options = if evaluated_args.len() == 3 {
8154 Some(&evaluated_args[2])
8155 } else {
8156 None
8157 };
8158 Ok(functions::numeric::format_number(*num, picture, options)?)
8159 }
8160 _ => Err(EvaluatorError::TypeError(
8161 "formatNumber() requires a number and a string".to_string(),
8162 )),
8163 }
8164 }
8165 "formatBase" => {
8166 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8167 return Err(EvaluatorError::EvaluationError(
8168 "formatBase() requires 1 or 2 arguments".to_string(),
8169 ));
8170 }
8171 // Handle undefined input
8172 if evaluated_args[0].is_null() {
8173 return Ok(JValue::Null);
8174 }
8175 if evaluated_args[0].is_undefined() {
8176 return Ok(JValue::Undefined);
8177 }
8178 match &evaluated_args[0] {
8179 JValue::Number(num) => {
8180 let radix = if evaluated_args.len() == 2 {
8181 match &evaluated_args[1] {
8182 JValue::Number(r) => Some(r.trunc() as i64),
8183 _ => {
8184 return Err(EvaluatorError::TypeError(
8185 "formatBase() radix must be a number".to_string(),
8186 ))
8187 }
8188 }
8189 } else {
8190 None
8191 };
8192 Ok(functions::numeric::format_base(*num, radix)?)
8193 }
8194 _ => Err(EvaluatorError::TypeError(
8195 "formatBase() requires a number".to_string(),
8196 )),
8197 }
8198 }
8199 "formatInteger" => {
8200 if evaluated_args.len() != 2 {
8201 return Err(EvaluatorError::EvaluationError(
8202 "formatInteger() requires exactly 2 arguments".to_string(),
8203 ));
8204 }
8205 match (&evaluated_args[0], &evaluated_args[1]) {
8206 (JValue::Number(n), JValue::String(picture)) => {
8207 Ok(crate::datetime::format_integer(*n, picture)?)
8208 }
8209 (JValue::Null, _) => Ok(JValue::Null),
8210 (JValue::Undefined, _) => Ok(JValue::Undefined),
8211 _ => Err(EvaluatorError::TypeError(
8212 "formatInteger() requires a number and a string".to_string(),
8213 )),
8214 }
8215 }
8216 "parseInteger" => {
8217 if evaluated_args.len() != 2 {
8218 return Err(EvaluatorError::EvaluationError(
8219 "parseInteger() requires exactly 2 arguments".to_string(),
8220 ));
8221 }
8222 match (&evaluated_args[0], &evaluated_args[1]) {
8223 (JValue::String(value), JValue::String(picture)) => {
8224 Ok(crate::datetime::parse_integer(value, picture)?)
8225 }
8226 (JValue::Null, _) => Ok(JValue::Null),
8227 (JValue::Undefined, _) => Ok(JValue::Undefined),
8228 _ => Err(EvaluatorError::TypeError(
8229 "parseInteger() requires a string and a string".to_string(),
8230 )),
8231 }
8232 }
8233 "append" => {
8234 if evaluated_args.len() != 2 {
8235 return Err(EvaluatorError::EvaluationError(
8236 "append() requires exactly 2 arguments".to_string(),
8237 ));
8238 }
8239 // Handle null/undefined arguments
8240 let first = &evaluated_args[0];
8241 let second = &evaluated_args[1];
8242
8243 // If second arg is null/undefined, return first as-is (no change)
8244 if second.is_null() || second.is_undefined() {
8245 return Ok(first.clone());
8246 }
8247
8248 // If first arg is null/undefined, return second as-is (appending to nothing gives second)
8249 if first.is_null() || first.is_undefined() {
8250 return Ok(second.clone());
8251 }
8252
8253 // Convert both to arrays if needed, then append
8254 let arr = match first {
8255 JValue::Array(a) => a.to_vec(),
8256 other => vec![other.clone()], // Wrap non-array in array
8257 };
8258
8259 // Pre-check combined size before concatenating, mirroring
8260 // jsonata-js's append() (`arg1.length + arg2.length > options.sequence`).
8261 let second_len = match second {
8262 JValue::Array(a) => a.len(),
8263 _ => 1,
8264 };
8265 check_sequence_length(arr.len() + second_len, &self.options)?;
8266
8267 Ok(functions::array::append(&arr, second)?)
8268 }
8269 "reverse" => {
8270 if evaluated_args.len() != 1 {
8271 return Err(EvaluatorError::EvaluationError(
8272 "reverse() requires exactly 1 argument".to_string(),
8273 ));
8274 }
8275 match &evaluated_args[0] {
8276 JValue::Null => Ok(JValue::Null),
8277 JValue::Undefined => Ok(JValue::Undefined),
8278 JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
8279 _ => Err(EvaluatorError::TypeError(
8280 "reverse() requires an array argument".to_string(),
8281 )),
8282 }
8283 }
8284 "shuffle" => {
8285 if evaluated_args.len() != 1 {
8286 return Err(EvaluatorError::EvaluationError(
8287 "shuffle() requires exactly 1 argument".to_string(),
8288 ));
8289 }
8290 if evaluated_args[0].is_null() {
8291 return Ok(JValue::Null);
8292 }
8293 if evaluated_args[0].is_undefined() {
8294 return Ok(JValue::Undefined);
8295 }
8296 match &evaluated_args[0] {
8297 JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
8298 _ => Err(EvaluatorError::TypeError(
8299 "shuffle() requires an array argument".to_string(),
8300 )),
8301 }
8302 }
8303
8304 "sift" => {
8305 // $sift(object, function) or $sift(function) - filter object by predicate
8306 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8307 return Err(EvaluatorError::EvaluationError(
8308 "sift() requires 1 or 2 arguments".to_string(),
8309 ));
8310 }
8311
8312 // Determine which argument is the function
8313 let func_arg = if evaluated_args.len() == 1 {
8314 &args[0]
8315 } else {
8316 &args[1]
8317 };
8318
8319 // Detect how many parameters the callback expects
8320 let param_count = self.get_callback_param_count(func_arg);
8321
8322 // Helper function to sift a single object
8323 let sift_object = |evaluator: &mut Self,
8324 obj: &IndexMap<String, JValue>,
8325 func_node: &AstNode,
8326 context_data: &JValue,
8327 param_count: usize|
8328 -> Result<JValue, EvaluatorError> {
8329 // Only create the object value if callback uses 3 parameters
8330 let obj_value = if param_count >= 3 {
8331 Some(JValue::object(obj.clone()))
8332 } else {
8333 None
8334 };
8335
8336 let mut result = IndexMap::new();
8337 for (key, value) in obj.iter() {
8338 // Build argument list based on what callback expects
8339 let call_args = match param_count {
8340 1 => vec![value.clone()],
8341 2 => vec![value.clone(), JValue::string(key.clone())],
8342 _ => vec![
8343 value.clone(),
8344 JValue::string(key.clone()),
8345 obj_value.as_ref().unwrap().clone(),
8346 ],
8347 };
8348
8349 let pred_result =
8350 evaluator.apply_function(func_node, &call_args, context_data)?;
8351 if evaluator.is_truthy(&pred_result) {
8352 result.insert(key.clone(), value.clone());
8353 }
8354 }
8355 // Return undefined for empty results (will be filtered by function application)
8356 if result.is_empty() {
8357 Ok(JValue::Undefined)
8358 } else {
8359 Ok(JValue::object(result))
8360 }
8361 };
8362
8363 // Handle partial application - if only 1 arg, use current context as object
8364 if evaluated_args.len() == 1 {
8365 // $sift(function) - use current context data as object
8366 let data = &normalize_lazy(data)?;
8367 match data {
8368 JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
8369 JValue::Array(arr) => {
8370 // Map sift over each object in the array
8371 let mut results = Vec::new();
8372 for item in arr.iter() {
8373 let item = &normalize_lazy(item)?;
8374 if let JValue::Object(o) = item {
8375 let sifted = sift_object(self, o, &args[0], item, param_count)?;
8376 // sift_object returns undefined for empty results
8377 if !sifted.is_undefined() {
8378 results.push(sifted);
8379 }
8380 }
8381 }
8382 Ok(JValue::array(results))
8383 }
8384 JValue::Null => Ok(JValue::Null),
8385 _ => Ok(JValue::Undefined),
8386 }
8387 } else {
8388 // $sift(object, function)
8389 match &evaluated_args[0] {
8390 JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
8391 JValue::Null => Ok(JValue::Null),
8392 _ => Err(EvaluatorError::TypeError(
8393 "sift() first argument must be an object".to_string(),
8394 )),
8395 }
8396 }
8397 }
8398
8399 "zip" => {
8400 if evaluated_args.is_empty() {
8401 return Err(EvaluatorError::EvaluationError(
8402 "zip() requires at least 1 argument".to_string(),
8403 ));
8404 }
8405
8406 // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
8407 // If any argument is null/undefined, return empty array
8408 let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
8409 for arg in &evaluated_args {
8410 match arg {
8411 JValue::Array(arr) => {
8412 if arr.is_empty() {
8413 // Empty array means result is empty
8414 return Ok(JValue::array(vec![]));
8415 }
8416 arrays.push(arr.to_vec());
8417 }
8418 JValue::Null | JValue::Undefined => {
8419 // Null/undefined means result is empty
8420 return Ok(JValue::array(vec![]));
8421 }
8422 other => {
8423 // Wrap non-array values in single-element array
8424 arrays.push(vec![other.clone()]);
8425 }
8426 }
8427 }
8428
8429 if arrays.is_empty() {
8430 return Ok(JValue::array(vec![]));
8431 }
8432
8433 // Find the length of the shortest array
8434 let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
8435
8436 // Zip the arrays together
8437 let mut result = Vec::with_capacity(min_len);
8438 for i in 0..min_len {
8439 let mut tuple = Vec::with_capacity(arrays.len());
8440 for array in &arrays {
8441 tuple.push(array[i].clone());
8442 }
8443 result.push(JValue::array(tuple));
8444 }
8445
8446 Ok(JValue::array(result))
8447 }
8448
8449 "sort" => {
8450 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8451 return Err(EvaluatorError::EvaluationError(
8452 "sort() requires 1 or 2 arguments".to_string(),
8453 ));
8454 }
8455
8456 // Use pre-evaluated first argument (avoid double evaluation)
8457 let array_value = &evaluated_args[0];
8458
8459 // Handle undefined input
8460 if array_value.is_null() {
8461 return Ok(JValue::Null);
8462 }
8463 if array_value.is_undefined() {
8464 return Ok(JValue::Undefined);
8465 }
8466
8467 let mut arr = match array_value {
8468 JValue::Array(arr) => arr.to_vec(),
8469 other => vec![other.clone()],
8470 };
8471
8472 if args.len() == 2 {
8473 // Sort using the comparator from raw args (need unevaluated lambda AST)
8474 // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
8475 self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
8476 Ok(JValue::array(arr))
8477 } else {
8478 // Default sort (no comparator)
8479 Ok(functions::array::sort(&arr)?)
8480 }
8481 }
8482 "distinct" => {
8483 if evaluated_args.len() != 1 {
8484 return Err(EvaluatorError::EvaluationError(
8485 "distinct() requires exactly 1 argument".to_string(),
8486 ));
8487 }
8488 match &evaluated_args[0] {
8489 JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
8490 // Non-array input, and arrays of length <= 1, pass through
8491 // unchanged (jsonata-js functions.js:
8492 // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
8493 other => Ok(other.clone()),
8494 }
8495 }
8496 "exists" => {
8497 if evaluated_args.len() != 1 {
8498 return Err(EvaluatorError::EvaluationError(
8499 "exists() requires exactly 1 argument".to_string(),
8500 ));
8501 }
8502 Ok(functions::array::exists(&evaluated_args[0])?)
8503 }
8504 "keys" => {
8505 if evaluated_args.len() != 1 {
8506 return Err(EvaluatorError::EvaluationError(
8507 "keys() requires exactly 1 argument".to_string(),
8508 ));
8509 }
8510
8511 // Helper to unwrap single-element arrays
8512 let unwrap_single = |keys: Vec<JValue>| -> JValue {
8513 if keys.len() == 1 {
8514 keys.into_iter().next().unwrap()
8515 } else {
8516 JValue::array(keys)
8517 }
8518 };
8519
8520 match &evaluated_args[0] {
8521 JValue::Null => Ok(JValue::Null),
8522 JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
8523 JValue::Object(obj) => {
8524 // Return undefined for empty objects
8525 if obj.is_empty() {
8526 Ok(JValue::Null)
8527 } else {
8528 let keys: Vec<JValue> =
8529 obj.keys().map(|k| JValue::string(k.clone())).collect();
8530 check_sequence_length(keys.len(), &self.options)?;
8531 Ok(unwrap_single(keys))
8532 }
8533 }
8534 JValue::Array(arr) => {
8535 // For arrays, collect keys from all objects
8536 let mut all_keys = Vec::new();
8537 for item in arr.iter() {
8538 // Skip lambda/builtin values
8539 if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
8540 continue;
8541 }
8542 let normalized_item = normalize_lazy(item)?;
8543 if let JValue::Object(obj) = &normalized_item {
8544 for key in obj.keys() {
8545 if !all_keys.contains(&JValue::string(key.clone())) {
8546 all_keys.push(JValue::string(key.clone()));
8547 }
8548 }
8549 }
8550 }
8551 if all_keys.is_empty() {
8552 Ok(JValue::Null)
8553 } else {
8554 check_sequence_length(all_keys.len(), &self.options)?;
8555 Ok(unwrap_single(all_keys))
8556 }
8557 }
8558 // Non-object types return undefined
8559 _ => Ok(JValue::Null),
8560 }
8561 }
8562 "lookup" => {
8563 if evaluated_args.len() != 2 {
8564 return Err(EvaluatorError::EvaluationError(
8565 "lookup() requires exactly 2 arguments".to_string(),
8566 ));
8567 }
8568 if evaluated_args[0].is_null() {
8569 return Ok(JValue::Null);
8570 }
8571 if evaluated_args[0].is_undefined() {
8572 return Ok(JValue::Undefined);
8573 }
8574
8575 let key = match &evaluated_args[1] {
8576 JValue::String(k) => &**k,
8577 _ => {
8578 return Err(EvaluatorError::TypeError(
8579 "lookup() requires a string key".to_string(),
8580 ))
8581 }
8582 };
8583
8584 // Helper function to recursively lookup in values
8585 fn lookup_recursive(
8586 val: &JValue,
8587 key: &str,
8588 ) -> Result<Vec<JValue>, EvaluatorError> {
8589 match val {
8590 JValue::Array(arr) => {
8591 let mut results = Vec::new();
8592 for item in arr.iter() {
8593 let nested = lookup_recursive(item, key)?;
8594 results.extend(nested.iter().cloned());
8595 }
8596 Ok(results)
8597 }
8598 JValue::Object(obj) => {
8599 if let Some(v) = obj.get(key) {
8600 Ok(vec![v.clone()])
8601 } else {
8602 Ok(vec![])
8603 }
8604 }
8605 #[cfg(feature = "python")]
8606 JValue::LazyPyDict(lazy) => {
8607 let v = lazy.get_field(key)?;
8608 if v.is_undefined() {
8609 Ok(vec![])
8610 } else {
8611 Ok(vec![v])
8612 }
8613 }
8614 _ => Ok(vec![]),
8615 }
8616 }
8617
8618 let results = lookup_recursive(&evaluated_args[0], key)?;
8619 if results.is_empty() {
8620 Ok(JValue::Null)
8621 } else if results.len() == 1 {
8622 Ok(results[0].clone())
8623 } else {
8624 check_sequence_length(results.len(), &self.options)?;
8625 Ok(JValue::array(results))
8626 }
8627 }
8628 "spread" => {
8629 if evaluated_args.len() != 1 {
8630 return Err(EvaluatorError::EvaluationError(
8631 "spread() requires exactly 1 argument".to_string(),
8632 ));
8633 }
8634 match &evaluated_args[0] {
8635 JValue::Null => Ok(JValue::Null),
8636 // Not a container - pass through unchanged (e.g. so $string() still
8637 // sees the function value and applies its own function->"" rule).
8638 lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
8639 JValue::Object(obj) => {
8640 // functions::object::spread() always returns an array with one
8641 // element per key (mirrors jsonata-js's push-per-key loop through
8642 // this.createSequence()), so it needs the same cap as the
8643 // array-fanout branch below and as the "keys" arm's single-object
8644 // branch.
8645 check_sequence_length(obj.len(), &self.options)?;
8646 Ok(functions::object::spread(obj)?)
8647 }
8648 JValue::Array(arr) => {
8649 // Spread each object in the array
8650 let mut result = Vec::new();
8651 for item in arr.iter() {
8652 let normalized_item = normalize_lazy(item)?;
8653 match &normalized_item {
8654 JValue::Lambda { .. } | JValue::Builtin { .. } => {
8655 // Skip lambdas in array
8656 continue;
8657 }
8658 JValue::Object(obj) => {
8659 let spread_result = functions::object::spread(obj)?;
8660 if let JValue::Array(spread_items) = spread_result {
8661 result.extend(spread_items.iter().cloned());
8662 } else {
8663 result.push(spread_result);
8664 }
8665 }
8666 // Non-objects in array are returned unchanged
8667 other => result.push(other.clone()),
8668 }
8669 }
8670 check_sequence_length(result.len(), &self.options)?;
8671 Ok(JValue::array(result))
8672 }
8673 // Non-objects are returned unchanged
8674 other => Ok(other.clone()),
8675 }
8676 }
8677 "merge" => {
8678 if evaluated_args.is_empty() {
8679 return Err(EvaluatorError::EvaluationError(
8680 "merge() requires at least 1 argument".to_string(),
8681 ));
8682 }
8683 // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
8684 // vs multiple object arguments: $merge(obj1, obj2)
8685 if evaluated_args.len() == 1 {
8686 match &evaluated_args[0] {
8687 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
8688 JValue::Null => Ok(JValue::Null),
8689 JValue::Undefined => Ok(JValue::Undefined),
8690 JValue::Object(_) => {
8691 // Single object - just return it
8692 Ok(evaluated_args[0].clone())
8693 }
8694 _ => Err(EvaluatorError::TypeError(
8695 "merge() requires objects or an array of objects".to_string(),
8696 )),
8697 }
8698 } else {
8699 Ok(functions::object::merge(&evaluated_args)?)
8700 }
8701 }
8702
8703 "map" => {
8704 if args.len() != 2 {
8705 return Err(EvaluatorError::EvaluationError(
8706 "map() requires exactly 2 arguments".to_string(),
8707 ));
8708 }
8709
8710 // Evaluate the array argument
8711 let array = self.evaluate_internal(&args[0], data)?;
8712
8713 match array {
8714 JValue::Array(arr) => {
8715 // Detect how many parameters the callback expects
8716 let param_count = self.get_callback_param_count(&args[1]);
8717
8718 // CompiledExpr fast path: direct lambda with 1 param, compilable body
8719 if param_count == 1 {
8720 if let AstNode::Lambda {
8721 params,
8722 body,
8723 signature: None,
8724 thunk: false,
8725 } = &args[1]
8726 {
8727 let var_refs: Vec<&str> =
8728 params.iter().map(|s| s.as_str()).collect();
8729 if let Some(compiled) =
8730 try_compile_expr_with_allowed_vars(body, &var_refs)
8731 {
8732 let param_name = params[0].as_str();
8733 let mut result = Vec::with_capacity(arr.len());
8734 let mut vars = HashMap::new();
8735 for item in arr.iter() {
8736 vars.insert(param_name, item);
8737 let mapped = eval_compiled(
8738 &compiled,
8739 data,
8740 Some(&vars),
8741 &self.options,
8742 self.start_time,
8743 )?;
8744 if !mapped.is_undefined() {
8745 result.push(mapped);
8746 }
8747 }
8748 check_sequence_length(result.len(), &self.options)?;
8749 return Ok(JValue::array(result));
8750 }
8751 }
8752 // Stored lambda variable fast path: $var with pre-compiled body
8753 if let AstNode::Variable(var_name) = &args[1] {
8754 if let Some(stored) = self.context.lookup_lambda(var_name) {
8755 if let Some(ref ce) = stored.compiled_body.clone() {
8756 let param_name = stored.params[0].clone();
8757 let captured_data = stored.captured_data.clone();
8758 let captured_env_clone = stored.captured_env.clone();
8759 let ce_clone = ce.clone();
8760 if !captured_env_clone.values().any(|v| {
8761 matches!(
8762 v,
8763 JValue::Lambda { .. } | JValue::Builtin { .. }
8764 )
8765 }) {
8766 let call_data = captured_data.as_ref().unwrap_or(data);
8767 let mut result = Vec::with_capacity(arr.len());
8768 let mut vars: HashMap<&str, &JValue> =
8769 captured_env_clone
8770 .iter()
8771 .map(|(k, v)| (k.as_str(), v))
8772 .collect();
8773 for item in arr.iter() {
8774 vars.insert(param_name.as_str(), item);
8775 let mapped = eval_compiled(
8776 &ce_clone,
8777 call_data,
8778 Some(&vars),
8779 &self.options,
8780 self.start_time,
8781 )?;
8782 if !mapped.is_undefined() {
8783 result.push(mapped);
8784 }
8785 }
8786 check_sequence_length(result.len(), &self.options)?;
8787 return Ok(JValue::array(result));
8788 }
8789 }
8790 }
8791 }
8792 }
8793
8794 // Only create the array value if callback uses 3 parameters
8795 let arr_value = if param_count >= 3 {
8796 Some(JValue::Array(arr.clone()))
8797 } else {
8798 None
8799 };
8800
8801 let mut result = Vec::with_capacity(arr.len());
8802 for (index, item) in arr.iter().enumerate() {
8803 // Build argument list based on what callback expects
8804 let call_args = match param_count {
8805 1 => vec![item.clone()],
8806 2 => vec![item.clone(), JValue::Number(index as f64)],
8807 _ => vec![
8808 item.clone(),
8809 JValue::Number(index as f64),
8810 arr_value.as_ref().unwrap().clone(),
8811 ],
8812 };
8813
8814 let mapped = self.apply_function(&args[1], &call_args, data)?;
8815 // Filter out undefined results but keep explicit null (JSONata map semantics)
8816 // undefined comes from missing else clause, null is explicit
8817 if !mapped.is_undefined() {
8818 result.push(mapped);
8819 }
8820 }
8821 check_sequence_length(result.len(), &self.options)?;
8822 Ok(JValue::array(result))
8823 }
8824 JValue::Null => Ok(JValue::Null),
8825 JValue::Undefined => Ok(JValue::Undefined),
8826 _ => Err(EvaluatorError::TypeError(
8827 "map() first argument must be an array".to_string(),
8828 )),
8829 }
8830 }
8831
8832 "filter" => {
8833 if args.len() != 2 {
8834 return Err(EvaluatorError::EvaluationError(
8835 "filter() requires exactly 2 arguments".to_string(),
8836 ));
8837 }
8838
8839 // Evaluate the array argument
8840 let array = self.evaluate_internal(&args[0], data)?;
8841
8842 // Handle undefined input - return undefined
8843 if array.is_undefined() {
8844 return Ok(JValue::Undefined);
8845 }
8846
8847 // Handle null input
8848 if array.is_null() {
8849 return Ok(JValue::Undefined);
8850 }
8851
8852 // Coerce non-array values to single-element arrays
8853 // Track if input was a single value to unwrap result appropriately
8854 // Use references to avoid upfront cloning of all elements
8855 let single_holder;
8856 let (items, was_single_value): (&[JValue], bool) = match &array {
8857 JValue::Array(arr) => (arr.as_slice(), false),
8858 _ => {
8859 single_holder = [array];
8860 (&single_holder[..], true)
8861 }
8862 };
8863
8864 // Detect how many parameters the callback expects
8865 let param_count = self.get_callback_param_count(&args[1]);
8866
8867 // CompiledExpr fast path: direct lambda with 1 param, compilable body
8868 if param_count == 1 {
8869 if let AstNode::Lambda {
8870 params,
8871 body,
8872 signature: None,
8873 thunk: false,
8874 } = &args[1]
8875 {
8876 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8877 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8878 {
8879 let param_name = params[0].as_str();
8880 let mut result = Vec::with_capacity(items.len() / 2);
8881 let mut vars = HashMap::new();
8882 for item in items.iter() {
8883 vars.insert(param_name, item);
8884 let pred_result = eval_compiled(
8885 &compiled,
8886 data,
8887 Some(&vars),
8888 &self.options,
8889 self.start_time,
8890 )?;
8891 if compiled_is_truthy(&pred_result) {
8892 result.push(item.clone());
8893 }
8894 }
8895 if was_single_value {
8896 if result.len() == 1 {
8897 return Ok(result.remove(0));
8898 } else if result.is_empty() {
8899 return Ok(JValue::Undefined);
8900 }
8901 }
8902 check_sequence_length(result.len(), &self.options)?;
8903 return Ok(JValue::array(result));
8904 }
8905 }
8906 // Stored lambda variable fast path: $var with pre-compiled body
8907 if let AstNode::Variable(var_name) = &args[1] {
8908 if let Some(stored) = self.context.lookup_lambda(var_name) {
8909 if let Some(ref ce) = stored.compiled_body.clone() {
8910 let param_name = stored.params[0].clone();
8911 let captured_data = stored.captured_data.clone();
8912 let captured_env_clone = stored.captured_env.clone();
8913 let ce_clone = ce.clone();
8914 if !captured_env_clone.values().any(|v| {
8915 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8916 }) {
8917 let call_data = captured_data.as_ref().unwrap_or(data);
8918 let mut result = Vec::with_capacity(items.len() / 2);
8919 let mut vars: HashMap<&str, &JValue> = captured_env_clone
8920 .iter()
8921 .map(|(k, v)| (k.as_str(), v))
8922 .collect();
8923 for item in items.iter() {
8924 vars.insert(param_name.as_str(), item);
8925 let pred_result = eval_compiled(
8926 &ce_clone,
8927 call_data,
8928 Some(&vars),
8929 &self.options,
8930 self.start_time,
8931 )?;
8932 if compiled_is_truthy(&pred_result) {
8933 result.push(item.clone());
8934 }
8935 }
8936 if was_single_value {
8937 if result.len() == 1 {
8938 return Ok(result.remove(0));
8939 } else if result.is_empty() {
8940 return Ok(JValue::Undefined);
8941 }
8942 }
8943 check_sequence_length(result.len(), &self.options)?;
8944 return Ok(JValue::array(result));
8945 }
8946 }
8947 }
8948 }
8949 }
8950
8951 // Only create the array value if callback uses 3 parameters
8952 let arr_value = if param_count >= 3 {
8953 Some(JValue::array(items.to_vec()))
8954 } else {
8955 None
8956 };
8957
8958 let mut result = Vec::with_capacity(items.len() / 2);
8959
8960 for (index, item) in items.iter().enumerate() {
8961 // Build argument list based on what callback expects
8962 let call_args = match param_count {
8963 1 => vec![item.clone()],
8964 2 => vec![item.clone(), JValue::Number(index as f64)],
8965 _ => vec![
8966 item.clone(),
8967 JValue::Number(index as f64),
8968 arr_value.as_ref().unwrap().clone(),
8969 ],
8970 };
8971
8972 let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8973 if self.is_truthy(&predicate_result) {
8974 result.push(item.clone());
8975 }
8976 }
8977
8978 // If input was a single value, return the single matching item
8979 // (or undefined if no match)
8980 if was_single_value {
8981 if result.len() == 1 {
8982 return Ok(result.remove(0));
8983 } else if result.is_empty() {
8984 return Ok(JValue::Undefined);
8985 }
8986 }
8987
8988 check_sequence_length(result.len(), &self.options)?;
8989 Ok(JValue::array(result))
8990 }
8991
8992 "reduce" => {
8993 if args.len() < 2 || args.len() > 3 {
8994 return Err(EvaluatorError::EvaluationError(
8995 "reduce() requires 2 or 3 arguments".to_string(),
8996 ));
8997 }
8998
8999 // Check that the callback function has at least 2 parameters
9000 if let AstNode::Lambda { params, .. } = &args[1] {
9001 if params.len() < 2 {
9002 return Err(EvaluatorError::EvaluationError(
9003 "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
9004 ));
9005 }
9006 } else if let AstNode::Function { name, .. } = &args[1] {
9007 // For now, we can't validate built-in function signatures here
9008 // But user-defined functions via lambda will be validated above
9009 let _ = name; // avoid unused warning
9010 }
9011
9012 // Evaluate the array argument
9013 let array = self.evaluate_internal(&args[0], data)?;
9014
9015 // Convert single value to array (JSONata reduce accepts single values)
9016 // Use references to avoid upfront cloning of all elements
9017 let single_holder;
9018 let items: &[JValue] = match &array {
9019 JValue::Array(arr) => arr.as_slice(),
9020 JValue::Null => return Ok(JValue::Null),
9021 _ => {
9022 single_holder = [array];
9023 &single_holder[..]
9024 }
9025 };
9026
9027 if items.is_empty() {
9028 // Return initial value if provided, otherwise null
9029 return if args.len() == 3 {
9030 self.evaluate_internal(&args[2], data)
9031 } else {
9032 Ok(JValue::Null)
9033 };
9034 }
9035
9036 // Get initial accumulator
9037 let mut accumulator = if args.len() == 3 {
9038 self.evaluate_internal(&args[2], data)?
9039 } else {
9040 items[0].clone()
9041 };
9042
9043 let start_idx = if args.len() == 3 { 0 } else { 1 };
9044
9045 // Detect how many parameters the callback expects
9046 let param_count = self.get_callback_param_count(&args[1]);
9047
9048 // CompiledExpr fast path: direct lambda with 2 params, compilable body
9049 if param_count == 2 {
9050 if let AstNode::Lambda {
9051 params,
9052 body,
9053 signature: None,
9054 thunk: false,
9055 } = &args[1]
9056 {
9057 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9058 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
9059 {
9060 let acc_name = params[0].as_str();
9061 let item_name = params[1].as_str();
9062 for item in items[start_idx..].iter() {
9063 let vars: HashMap<&str, &JValue> =
9064 HashMap::from([(acc_name, &accumulator), (item_name, item)]);
9065 accumulator = eval_compiled(
9066 &compiled,
9067 data,
9068 Some(&vars),
9069 &self.options,
9070 self.start_time,
9071 )?;
9072 }
9073 return Ok(accumulator);
9074 }
9075 }
9076 // Stored lambda variable fast path: $var with pre-compiled body
9077 if let AstNode::Variable(var_name) = &args[1] {
9078 if let Some(stored) = self.context.lookup_lambda(var_name) {
9079 if stored.params.len() == 2 {
9080 if let Some(ref ce) = stored.compiled_body.clone() {
9081 let acc_param = stored.params[0].clone();
9082 let item_param = stored.params[1].clone();
9083 let captured_data = stored.captured_data.clone();
9084 let captured_env_clone = stored.captured_env.clone();
9085 let ce_clone = ce.clone();
9086 if !captured_env_clone.values().any(|v| {
9087 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
9088 }) {
9089 let call_data = captured_data.as_ref().unwrap_or(data);
9090 for item in items[start_idx..].iter() {
9091 let mut vars: HashMap<&str, &JValue> =
9092 captured_env_clone
9093 .iter()
9094 .map(|(k, v)| (k.as_str(), v))
9095 .collect();
9096 vars.insert(acc_param.as_str(), &accumulator);
9097 vars.insert(item_param.as_str(), item);
9098 // Evaluate and drop vars before assigning accumulator
9099 // to satisfy borrow checker (vars borrows accumulator)
9100 let new_acc = eval_compiled(
9101 &ce_clone,
9102 call_data,
9103 Some(&vars),
9104 &self.options,
9105 self.start_time,
9106 )?;
9107 drop(vars);
9108 accumulator = new_acc;
9109 }
9110 return Ok(accumulator);
9111 }
9112 }
9113 }
9114 }
9115 }
9116 }
9117
9118 // Only create the array value if callback uses 4 parameters
9119 let arr_value = if param_count >= 4 {
9120 Some(JValue::array(items.to_vec()))
9121 } else {
9122 None
9123 };
9124
9125 // Apply function to each element
9126 for (idx, item) in items[start_idx..].iter().enumerate() {
9127 // For reduce, the function receives (accumulator, value, index, array)
9128 // Callbacks may use any subset of these parameters
9129 let actual_idx = start_idx + idx;
9130
9131 // Build argument list based on what callback expects
9132 let call_args = match param_count {
9133 2 => vec![accumulator.clone(), item.clone()],
9134 3 => vec![
9135 accumulator.clone(),
9136 item.clone(),
9137 JValue::Number(actual_idx as f64),
9138 ],
9139 _ => vec![
9140 accumulator.clone(),
9141 item.clone(),
9142 JValue::Number(actual_idx as f64),
9143 arr_value.as_ref().unwrap().clone(),
9144 ],
9145 };
9146
9147 accumulator = self.apply_function(&args[1], &call_args, data)?;
9148 }
9149
9150 Ok(accumulator)
9151 }
9152
9153 "single" => {
9154 if args.is_empty() || args.len() > 2 {
9155 return Err(EvaluatorError::EvaluationError(
9156 "single() requires 1 or 2 arguments".to_string(),
9157 ));
9158 }
9159
9160 // Evaluate the array argument
9161 let array = self.evaluate_internal(&args[0], data)?;
9162
9163 // Convert to array (wrap single values)
9164 let arr = match array {
9165 JValue::Array(arr) => arr.to_vec(),
9166 JValue::Null => return Ok(JValue::Null),
9167 other => vec![other],
9168 };
9169
9170 if args.len() == 1 {
9171 // No predicate - array must have exactly 1 element
9172 match arr.len() {
9173 0 => Err(EvaluatorError::EvaluationError(
9174 "single() argument is empty".to_string(),
9175 )),
9176 1 => Ok(arr.into_iter().next().unwrap()),
9177 count => Err(EvaluatorError::EvaluationError(format!(
9178 "single() argument has {} values (expected exactly 1)",
9179 count
9180 ))),
9181 }
9182 } else {
9183 // With predicate - find exactly 1 matching element
9184 let arr_value = JValue::array(arr.clone());
9185 let mut matches = Vec::new();
9186 for (index, item) in arr.into_iter().enumerate() {
9187 // Apply predicate function with (item, index, array)
9188 let predicate_result = self.apply_function(
9189 &args[1],
9190 &[
9191 item.clone(),
9192 JValue::Number(index as f64),
9193 arr_value.clone(),
9194 ],
9195 data,
9196 )?;
9197 if self.is_truthy(&predicate_result) {
9198 matches.push(item);
9199 }
9200 }
9201
9202 match matches.len() {
9203 0 => Err(EvaluatorError::EvaluationError(
9204 "single() predicate matches no values".to_string(),
9205 )),
9206 1 => Ok(matches.into_iter().next().unwrap()),
9207 count => Err(EvaluatorError::EvaluationError(format!(
9208 "single() predicate matches {} values (expected exactly 1)",
9209 count
9210 ))),
9211 }
9212 }
9213 }
9214
9215 "each" => {
9216 // $each(object, function) - iterate over object, applying function to each value/key pair
9217 // Returns an array of the function results
9218 if args.is_empty() || args.len() > 2 {
9219 return Err(EvaluatorError::EvaluationError(
9220 "each() requires 1 or 2 arguments".to_string(),
9221 ));
9222 }
9223
9224 // Determine which argument is the object and which is the function
9225 let (obj_value, func_arg) = if args.len() == 1 {
9226 // Single argument: use current data as object
9227 (data.clone(), &args[0])
9228 } else {
9229 // Two arguments: first is object, second is function
9230 (self.evaluate_internal(&args[0], data)?, &args[1])
9231 };
9232
9233 // Detect how many parameters the callback expects
9234 let param_count = self.get_callback_param_count(func_arg);
9235
9236 let obj_value = normalize_lazy(&obj_value)?;
9237
9238 match obj_value {
9239 JValue::Object(obj) => {
9240 let mut result = Vec::new();
9241 for (key, value) in obj.iter() {
9242 // Build argument list based on what callback expects
9243 // The callback receives the value as the first argument and key as second
9244 let call_args = match param_count {
9245 1 => vec![value.clone()],
9246 _ => vec![value.clone(), JValue::string(key.clone())],
9247 };
9248
9249 let fn_result = self.apply_function(func_arg, &call_args, data)?;
9250 // Skip undefined results (similar to map behavior)
9251 if !fn_result.is_null() && !fn_result.is_undefined() {
9252 result.push(fn_result);
9253 }
9254 }
9255 check_sequence_length(result.len(), &self.options)?;
9256 Ok(JValue::array(result))
9257 }
9258 JValue::Null => Ok(JValue::Null),
9259 _ => Err(EvaluatorError::TypeError(
9260 "each() first argument must be an object".to_string(),
9261 )),
9262 }
9263 }
9264
9265 "not" => {
9266 if evaluated_args.len() != 1 {
9267 return Err(EvaluatorError::EvaluationError(
9268 "not() requires exactly 1 argument".to_string(),
9269 ));
9270 }
9271 // $not(x) returns the logical negation of x
9272 // null is falsy, so $not(null) = true; undefined stays undefined
9273 if evaluated_args[0].is_undefined() {
9274 return Ok(JValue::Undefined);
9275 }
9276 Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
9277 }
9278 "boolean" => {
9279 if evaluated_args.len() != 1 {
9280 return Err(EvaluatorError::EvaluationError(
9281 "boolean() requires exactly 1 argument".to_string(),
9282 ));
9283 }
9284 if evaluated_args[0].is_undefined() {
9285 return Ok(JValue::Undefined);
9286 }
9287 Ok(functions::boolean::boolean(&evaluated_args[0])?)
9288 }
9289 "type" => {
9290 if evaluated_args.len() != 1 {
9291 return Err(EvaluatorError::EvaluationError(
9292 "type() requires exactly 1 argument".to_string(),
9293 ));
9294 }
9295 // Return type string
9296 // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
9297 // We use a special marker object to distinguish undefined from null
9298 match &evaluated_args[0] {
9299 JValue::Null => Ok(JValue::string("null")),
9300 JValue::Bool(_) => Ok(JValue::string("boolean")),
9301 JValue::Number(_) => Ok(JValue::string("number")),
9302 JValue::String(_) => Ok(JValue::string("string")),
9303 JValue::Array(_) => Ok(JValue::string("array")),
9304 JValue::Object(_) => Ok(JValue::string("object")),
9305 JValue::Undefined => Ok(JValue::Undefined),
9306 JValue::Lambda { .. } | JValue::Builtin { .. } => {
9307 Ok(JValue::string("function"))
9308 }
9309 JValue::Regex { .. } => Ok(JValue::string("regex")),
9310 #[cfg(feature = "python")]
9311 JValue::LazyPyDict(_) => Ok(JValue::string("object")),
9312 }
9313 }
9314
9315 "base64encode" => {
9316 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
9317 return Ok(JValue::Null);
9318 }
9319 if evaluated_args.len() != 1 {
9320 return Err(EvaluatorError::EvaluationError(
9321 "base64encode() requires exactly 1 argument".to_string(),
9322 ));
9323 }
9324 match &evaluated_args[0] {
9325 JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
9326 _ => Err(EvaluatorError::TypeError(
9327 "base64encode() requires a string argument".to_string(),
9328 )),
9329 }
9330 }
9331 "base64decode" => {
9332 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
9333 return Ok(JValue::Null);
9334 }
9335 if evaluated_args.len() != 1 {
9336 return Err(EvaluatorError::EvaluationError(
9337 "base64decode() requires exactly 1 argument".to_string(),
9338 ));
9339 }
9340 match &evaluated_args[0] {
9341 JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
9342 _ => Err(EvaluatorError::TypeError(
9343 "base64decode() requires a string argument".to_string(),
9344 )),
9345 }
9346 }
9347 "encodeUrlComponent" => {
9348 if evaluated_args.len() != 1 {
9349 return Err(EvaluatorError::EvaluationError(
9350 "encodeUrlComponent() requires exactly 1 argument".to_string(),
9351 ));
9352 }
9353 if evaluated_args[0].is_null() {
9354 return Ok(JValue::Null);
9355 }
9356 if evaluated_args[0].is_undefined() {
9357 return Ok(JValue::Undefined);
9358 }
9359 match &evaluated_args[0] {
9360 JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
9361 _ => Err(EvaluatorError::TypeError(
9362 "encodeUrlComponent() requires a string argument".to_string(),
9363 )),
9364 }
9365 }
9366 "decodeUrlComponent" => {
9367 if evaluated_args.len() != 1 {
9368 return Err(EvaluatorError::EvaluationError(
9369 "decodeUrlComponent() requires exactly 1 argument".to_string(),
9370 ));
9371 }
9372 if evaluated_args[0].is_null() {
9373 return Ok(JValue::Null);
9374 }
9375 if evaluated_args[0].is_undefined() {
9376 return Ok(JValue::Undefined);
9377 }
9378 match &evaluated_args[0] {
9379 JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
9380 _ => Err(EvaluatorError::TypeError(
9381 "decodeUrlComponent() requires a string argument".to_string(),
9382 )),
9383 }
9384 }
9385 "encodeUrl" => {
9386 if evaluated_args.len() != 1 {
9387 return Err(EvaluatorError::EvaluationError(
9388 "encodeUrl() requires exactly 1 argument".to_string(),
9389 ));
9390 }
9391 if evaluated_args[0].is_null() {
9392 return Ok(JValue::Null);
9393 }
9394 if evaluated_args[0].is_undefined() {
9395 return Ok(JValue::Undefined);
9396 }
9397 match &evaluated_args[0] {
9398 JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
9399 _ => Err(EvaluatorError::TypeError(
9400 "encodeUrl() requires a string argument".to_string(),
9401 )),
9402 }
9403 }
9404 "decodeUrl" => {
9405 if evaluated_args.len() != 1 {
9406 return Err(EvaluatorError::EvaluationError(
9407 "decodeUrl() requires exactly 1 argument".to_string(),
9408 ));
9409 }
9410 if evaluated_args[0].is_null() {
9411 return Ok(JValue::Null);
9412 }
9413 if evaluated_args[0].is_undefined() {
9414 return Ok(JValue::Undefined);
9415 }
9416 match &evaluated_args[0] {
9417 JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
9418 _ => Err(EvaluatorError::TypeError(
9419 "decodeUrl() requires a string argument".to_string(),
9420 )),
9421 }
9422 }
9423
9424 "error" => {
9425 // $error(message) - throw error with custom message
9426 if evaluated_args.is_empty() {
9427 // No message provided
9428 return Err(EvaluatorError::EvaluationError(
9429 "D3137: $error() function evaluated".to_string(),
9430 ));
9431 }
9432
9433 match &evaluated_args[0] {
9434 JValue::String(s) => {
9435 Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
9436 }
9437 _ => Err(EvaluatorError::TypeError(
9438 "T0410: Argument 1 of function error does not match function signature"
9439 .to_string(),
9440 )),
9441 }
9442 }
9443 "assert" => {
9444 // $assert(condition, message) - throw error if condition is false
9445 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9446 return Err(EvaluatorError::EvaluationError(
9447 "assert() requires 1 or 2 arguments".to_string(),
9448 ));
9449 }
9450
9451 // First argument must be a boolean
9452 let condition = match &evaluated_args[0] {
9453 JValue::Bool(b) => *b,
9454 _ => {
9455 return Err(EvaluatorError::TypeError(
9456 "T0410: Argument 1 of function $assert does not match function signature".to_string(),
9457 ));
9458 }
9459 };
9460
9461 if !condition {
9462 let message = if evaluated_args.len() == 2 {
9463 match &evaluated_args[1] {
9464 JValue::String(s) => s.clone(),
9465 _ => Rc::from("$assert() statement failed"),
9466 }
9467 } else {
9468 Rc::from("$assert() statement failed")
9469 };
9470 return Err(EvaluatorError::EvaluationError(format!(
9471 "D3141: {}",
9472 message
9473 )));
9474 }
9475
9476 Ok(JValue::Null)
9477 }
9478
9479 "eval" => {
9480 // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
9481 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9482 return Err(EvaluatorError::EvaluationError(
9483 "T0410: Argument 1 of function $eval must be a string".to_string(),
9484 ));
9485 }
9486
9487 // If the first argument is null/undefined, return undefined
9488 if evaluated_args[0].is_null() {
9489 return Ok(JValue::Null);
9490 }
9491 if evaluated_args[0].is_undefined() {
9492 return Ok(JValue::Undefined);
9493 }
9494
9495 // First argument must be a string expression
9496 let expr_str = match &evaluated_args[0] {
9497 JValue::String(s) => &**s,
9498 _ => {
9499 return Err(EvaluatorError::EvaluationError(
9500 "T0410: Argument 1 of function $eval must be a string".to_string(),
9501 ));
9502 }
9503 };
9504
9505 // Parse the expression
9506 let parsed_ast = match parser::parse(expr_str) {
9507 Ok(ast) => ast,
9508 Err(e) => {
9509 // D3120 is the error code for parse errors in $eval
9510 return Err(EvaluatorError::EvaluationError(format!(
9511 "D3120: The expression passed to $eval cannot be parsed: {}",
9512 e
9513 )));
9514 }
9515 };
9516
9517 // Determine the context to use for evaluation
9518 let eval_context = if evaluated_args.len() == 2 {
9519 &evaluated_args[1]
9520 } else {
9521 data
9522 };
9523
9524 // Evaluate the parsed expression
9525 match self.evaluate_internal(&parsed_ast, eval_context) {
9526 Ok(result) => Ok(result),
9527 Err(e) => {
9528 // D3121 is the error code for evaluation errors in $eval
9529 let err_msg = e.to_string();
9530 if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
9531 Err(EvaluatorError::EvaluationError(format!(
9532 "D3121: {}",
9533 err_msg
9534 )))
9535 } else {
9536 Err(e)
9537 }
9538 }
9539 }
9540 }
9541
9542 "now" => {
9543 if !evaluated_args.is_empty() {
9544 return Err(EvaluatorError::EvaluationError(
9545 "now() takes no arguments".to_string(),
9546 ));
9547 }
9548 Ok(crate::datetime::now())
9549 }
9550
9551 "millis" => {
9552 if !evaluated_args.is_empty() {
9553 return Err(EvaluatorError::EvaluationError(
9554 "millis() takes no arguments".to_string(),
9555 ));
9556 }
9557 Ok(crate::datetime::millis())
9558 }
9559
9560 "toMillis" => {
9561 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
9562 return Err(EvaluatorError::EvaluationError(
9563 "toMillis() requires 1 or 2 arguments".to_string(),
9564 ));
9565 }
9566
9567 match &evaluated_args[0] {
9568 JValue::String(s) => {
9569 // Optional second argument is a picture string for custom parsing
9570 if evaluated_args.len() == 2 {
9571 match &evaluated_args[1] {
9572 JValue::String(picture) => {
9573 // Use custom picture format parsing
9574 Ok(crate::datetime::to_millis_with_picture(s, picture)?)
9575 }
9576 JValue::Null => Ok(JValue::Null),
9577 JValue::Undefined => Ok(JValue::Undefined),
9578 _ => Err(EvaluatorError::TypeError(
9579 "toMillis() second argument must be a string".to_string(),
9580 )),
9581 }
9582 } else {
9583 // Use ISO 8601 partial date parsing
9584 Ok(crate::datetime::to_millis(s)?)
9585 }
9586 }
9587 JValue::Null => Ok(JValue::Null),
9588 JValue::Undefined => Ok(JValue::Undefined),
9589 _ => Err(EvaluatorError::TypeError(
9590 "toMillis() requires a string argument".to_string(),
9591 )),
9592 }
9593 }
9594
9595 "fromMillis" => {
9596 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
9597 return Err(EvaluatorError::EvaluationError(
9598 "fromMillis() requires 1 to 3 arguments".to_string(),
9599 ));
9600 }
9601
9602 match &evaluated_args[0] {
9603 JValue::Number(n) => {
9604 let millis = (if n.fract() == 0.0 {
9605 Ok(*n as i64)
9606 } else {
9607 Err(())
9608 })
9609 .map_err(|_| {
9610 EvaluatorError::TypeError(
9611 "fromMillis() requires an integer".to_string(),
9612 )
9613 })?;
9614
9615 let picture = match evaluated_args.get(1) {
9616 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9617 Some(JValue::String(s)) => Some(s.to_string()),
9618 Some(_) => {
9619 return Err(EvaluatorError::TypeError(
9620 "fromMillis() second argument must be a string".to_string(),
9621 ))
9622 }
9623 };
9624 let timezone = match evaluated_args.get(2) {
9625 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
9626 Some(JValue::String(s)) => Some(s.to_string()),
9627 Some(_) => {
9628 return Err(EvaluatorError::TypeError(
9629 "fromMillis() third argument must be a string".to_string(),
9630 ))
9631 }
9632 };
9633
9634 Ok(crate::datetime::from_millis_with_picture(
9635 millis,
9636 picture.as_deref(),
9637 timezone.as_deref(),
9638 )?)
9639 }
9640 JValue::Null => Ok(JValue::Null),
9641 JValue::Undefined => Ok(JValue::Undefined),
9642 _ => Err(EvaluatorError::TypeError(
9643 "fromMillis() requires a number argument".to_string(),
9644 )),
9645 }
9646 }
9647
9648 _ => Err(EvaluatorError::ReferenceError(format!(
9649 "Unknown function: {}",
9650 name
9651 ))),
9652 }
9653 }
9654
9655 /// Apply a function (lambda or expression) to values
9656 ///
9657 /// This handles both:
9658 /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
9659 /// 2. Simple expressions: price * 2 - evaluates with values as context
9660 fn apply_function(
9661 &mut self,
9662 func_node: &AstNode,
9663 values: &[JValue],
9664 data: &JValue,
9665 ) -> Result<JValue, EvaluatorError> {
9666 match func_node {
9667 AstNode::Lambda {
9668 params,
9669 body,
9670 signature,
9671 thunk,
9672 } => {
9673 // Direct lambda - invoke it
9674 self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
9675 }
9676 AstNode::Function {
9677 name,
9678 args,
9679 is_builtin,
9680 } => {
9681 // Function call - check if it has placeholders (partial application)
9682 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
9683
9684 if has_placeholder {
9685 // This is a partial application - evaluate it to get the lambda value
9686 let partial_lambda =
9687 self.create_partial_application(name, args, *is_builtin, data)?;
9688
9689 // Now invoke the partial lambda with the provided values
9690 if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
9691 return self.invoke_stored_lambda(&stored, values, data);
9692 }
9693 Err(EvaluatorError::EvaluationError(
9694 "Failed to apply partial application".to_string(),
9695 ))
9696 } else {
9697 // Regular function call without placeholders
9698 // Evaluate it and apply if it returns a function
9699 let result = self.evaluate_internal(func_node, data)?;
9700
9701 // Check if result is a lambda value
9702 if let Some(stored) = self.lookup_lambda_from_value(&result) {
9703 return self.invoke_stored_lambda(&stored, values, data);
9704 }
9705
9706 // Otherwise just return the result
9707 Ok(result)
9708 }
9709 }
9710 AstNode::Variable(var_name) => {
9711 // Check if this variable holds a stored lambda
9712 if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
9713 self.invoke_stored_lambda(&stored_lambda, values, data)
9714 } else if let Some(value) = self.context.lookup(var_name).cloned() {
9715 // Check if this variable holds a lambda value
9716 // This handles lambdas passed as bound arguments in partial applications
9717 if let Some(stored) = self.lookup_lambda_from_value(&value) {
9718 return self.invoke_stored_lambda(&stored, values, data);
9719 }
9720 // Regular variable value - evaluate with first value as context
9721 if values.is_empty() {
9722 self.evaluate_internal(func_node, data)
9723 } else {
9724 self.evaluate_internal(func_node, &values[0])
9725 }
9726 } else if self.is_builtin_function(var_name) {
9727 // This is a built-in function reference (e.g., $string, $number)
9728 // Call it directly with the provided values (already evaluated)
9729 self.call_builtin_with_values(var_name, values)
9730 } else {
9731 // Unknown variable - evaluate with first value as context
9732 if values.is_empty() {
9733 self.evaluate_internal(func_node, data)
9734 } else {
9735 self.evaluate_internal(func_node, &values[0])
9736 }
9737 }
9738 }
9739 _ => {
9740 // For non-lambda expressions, evaluate with first value as context
9741 if values.is_empty() {
9742 self.evaluate_internal(func_node, data)
9743 } else {
9744 self.evaluate_internal(func_node, &values[0])
9745 }
9746 }
9747 }
9748 }
9749
9750 /// Execute a transform operator on the bound $ value
9751 fn execute_transform(
9752 &mut self,
9753 location: &AstNode,
9754 update: &AstNode,
9755 delete: Option<&AstNode>,
9756 _original_data: &JValue,
9757 ) -> Result<JValue, EvaluatorError> {
9758 // Get the input value from $ binding
9759 let input = self
9760 .context
9761 .lookup("$")
9762 .ok_or_else(|| {
9763 EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
9764 })?
9765 .clone();
9766
9767 // Evaluate location expression on the input to get objects to transform
9768 let located_objects = self.evaluate_internal(location, &input)?;
9769
9770 // Collect target objects into a vector for comparison
9771 let targets: Vec<JValue> = match located_objects {
9772 JValue::Array(arr) => arr.to_vec(),
9773 JValue::Object(_) => vec![located_objects],
9774 #[cfg(feature = "python")]
9775 JValue::LazyPyDict(_) => vec![located_objects],
9776 JValue::Null => Vec::new(),
9777 other => vec![other],
9778 };
9779
9780 // Validate update parameter - must be an object constructor
9781 // We need to check this before evaluation in case of errors
9782 // For now, we'll validate after evaluation in the transform helper
9783
9784 // Parse delete field names if provided
9785 let delete_fields: Vec<String> = if let Some(delete_node) = delete {
9786 let delete_val = self.evaluate_internal(delete_node, &input)?;
9787 match delete_val {
9788 JValue::Array(arr) => arr
9789 .iter()
9790 .filter_map(|v| match v {
9791 JValue::String(s) => Some(s.to_string()),
9792 _ => None,
9793 })
9794 .collect(),
9795 JValue::String(s) => vec![s.to_string()],
9796 JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
9797 _ => {
9798 // Delete parameter must be an array of strings or a string
9799 return Err(EvaluatorError::EvaluationError(
9800 "T2012: The third argument of the transform operator must be an array of strings".to_string()
9801 ));
9802 }
9803 }
9804 } else {
9805 Vec::new()
9806 };
9807
9808 // Recursive helper to apply transformation throughout the structure
9809 fn apply_transform_deep(
9810 evaluator: &mut Evaluator,
9811 value: &JValue,
9812 targets: &[JValue],
9813 update: &AstNode,
9814 delete_fields: &[String],
9815 ) -> Result<JValue, EvaluatorError> {
9816 // Check if this value is one of the targets to transform
9817 // Use JValue's PartialEq for semantic equality comparison
9818 if targets.iter().any(|t| t == value) {
9819 // Transform this object
9820 let value = &normalize_lazy(value)?;
9821 if let JValue::Object(map_rc) = value.clone() {
9822 let mut map = (*map_rc).clone();
9823 let update_val = evaluator.evaluate_internal(update, value)?;
9824 // Validate that update evaluates to an object or null (undefined)
9825 match update_val {
9826 JValue::Object(update_map) => {
9827 for (key, val) in update_map.iter() {
9828 map.insert(key.clone(), val.clone());
9829 }
9830 }
9831 JValue::Null | JValue::Undefined => {
9832 // Null/undefined means no updates, just continue to deletions
9833 }
9834 _ => {
9835 return Err(EvaluatorError::EvaluationError(
9836 "T2011: The second argument of the transform operator must evaluate to an object".to_string()
9837 ));
9838 }
9839 }
9840 for field in delete_fields {
9841 map.shift_remove(field);
9842 }
9843 return Ok(JValue::object(map));
9844 }
9845 return Ok(value.clone());
9846 }
9847
9848 // Otherwise, recursively process children to find and transform targets
9849 match value {
9850 JValue::Object(map) => {
9851 let mut new_map = IndexMap::new();
9852 for (k, v) in map.iter() {
9853 new_map.insert(
9854 k.clone(),
9855 apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
9856 );
9857 }
9858 Ok(JValue::object(new_map))
9859 }
9860 #[cfg(feature = "python")]
9861 JValue::LazyPyDict(lazy) => {
9862 let obj = JValue::Object(lazy.to_object().map_err(EvaluatorError::from)?);
9863 apply_transform_deep(evaluator, &obj, targets, update, delete_fields)
9864 }
9865 JValue::Array(arr) => {
9866 let mut new_arr = Vec::new();
9867 for item in arr.iter() {
9868 new_arr.push(apply_transform_deep(
9869 evaluator,
9870 item,
9871 targets,
9872 update,
9873 delete_fields,
9874 )?);
9875 }
9876 Ok(JValue::array(new_arr))
9877 }
9878 _ => Ok(value.clone()),
9879 }
9880 }
9881
9882 // Apply transformation recursively starting from input
9883 apply_transform_deep(self, &input, &targets, update, &delete_fields)
9884 }
9885
9886 /// Helper to invoke a lambda with given parameters
9887 fn invoke_lambda(
9888 &mut self,
9889 params: &[String],
9890 body: &AstNode,
9891 signature: Option<&String>,
9892 values: &[JValue],
9893 data: &JValue,
9894 thunk: bool,
9895 ) -> Result<JValue, EvaluatorError> {
9896 self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
9897 }
9898
9899 /// Invoke a lambda with optional captured environment (for closures)
9900 fn invoke_lambda_with_env(
9901 &mut self,
9902 params: &[String],
9903 body: &AstNode,
9904 signature: Option<&String>,
9905 values: &[JValue],
9906 data: &JValue,
9907 captured_env: Option<&HashMap<String, JValue>>,
9908 captured_data: Option<&JValue>,
9909 thunk: bool,
9910 ) -> Result<JValue, EvaluatorError> {
9911 // If this is a thunk (has tail calls), use TCO trampoline
9912 if thunk {
9913 let stored = StoredLambda {
9914 params: params.to_vec(),
9915 body: body.clone(),
9916 compiled_body: None, // Thunks use TCO, not the compiled fast path
9917 signature: signature.cloned(),
9918 captured_env: captured_env.cloned().unwrap_or_default(),
9919 captured_data: captured_data.cloned(),
9920 thunk,
9921 };
9922 return self.invoke_lambda_with_tco(&stored, values, data);
9923 }
9924
9925 // Validate signature if present, and get coerced arguments
9926 // Push a new scope for this lambda invocation
9927 self.context.push_scope();
9928
9929 // First apply captured environment (for closures)
9930 if let Some(env) = captured_env {
9931 for (name, value) in env {
9932 self.context.bind(name.clone(), value.clone());
9933 }
9934 }
9935
9936 if let Some(sig_str) = signature {
9937 // Validate and coerce arguments with signature
9938 let coerced_values = match crate::signature::Signature::parse(sig_str) {
9939 Ok(sig) => match sig.validate_and_coerce(values, data) {
9940 Ok(coerced) => coerced,
9941 Err(e) => {
9942 self.context.pop_scope();
9943 match e {
9944 crate::signature::SignatureError::UndefinedArgument => {
9945 return Ok(JValue::Null);
9946 }
9947 crate::signature::SignatureError::ArgumentTypeMismatch {
9948 index,
9949 expected,
9950 } => {
9951 return Err(EvaluatorError::TypeError(
9952 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9953 ));
9954 }
9955 crate::signature::SignatureError::ArrayTypeMismatch {
9956 index,
9957 expected,
9958 } => {
9959 return Err(EvaluatorError::TypeError(format!(
9960 "T0412: Argument {} of function must be an array of {}",
9961 index, expected
9962 )));
9963 }
9964 crate::signature::SignatureError::ContextTypeMismatch {
9965 index,
9966 expected,
9967 } => {
9968 return Err(EvaluatorError::TypeError(format!(
9969 "T0411: Context value at argument {} does not match function signature (expected {})",
9970 index, expected
9971 )));
9972 }
9973 _ => {
9974 return Err(EvaluatorError::TypeError(format!(
9975 "Signature validation failed: {}",
9976 e
9977 )));
9978 }
9979 }
9980 }
9981 },
9982 Err(e) => {
9983 self.context.pop_scope();
9984 return Err(EvaluatorError::EvaluationError(format!(
9985 "Invalid signature: {}",
9986 e
9987 )));
9988 }
9989 };
9990 // Bind coerced values to params
9991 for (i, param) in params.iter().enumerate() {
9992 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9993 self.context.bind(param.clone(), value);
9994 }
9995 } else {
9996 // No signature - bind directly from values slice (no allocation)
9997 for (i, param) in params.iter().enumerate() {
9998 let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9999 self.context.bind(param.clone(), value);
10000 }
10001 }
10002
10003 // Check if this is a partial application (body is a special marker string)
10004 if let AstNode::String(body_str) = body {
10005 if body_str.starts_with("__partial_call:") {
10006 // Parse the partial call info
10007 let parts: Vec<&str> = body_str.split(':').collect();
10008 if parts.len() >= 4 {
10009 let func_name = parts[1];
10010 let is_builtin = parts[2] == "true";
10011 let total_args: usize = parts[3].parse().unwrap_or(0);
10012
10013 // Get placeholder positions from captured env
10014 let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
10015 if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
10016 positions
10017 .iter()
10018 .filter_map(|v| v.as_f64().map(|n| n as usize))
10019 .collect()
10020 } else {
10021 vec![]
10022 }
10023 } else {
10024 vec![]
10025 };
10026
10027 // Reconstruct the full argument list
10028 let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
10029
10030 // Fill in bound arguments from captured environment
10031 if let Some(env) = captured_env {
10032 for (key, value) in env {
10033 if key.starts_with("__bound_arg_") {
10034 if let Ok(pos) = key[12..].parse::<usize>() {
10035 if pos < total_args {
10036 full_args[pos] = value.clone();
10037 }
10038 }
10039 }
10040 }
10041 }
10042
10043 // Fill in placeholder positions with provided values
10044 for (i, &pos) in placeholder_positions.iter().enumerate() {
10045 if pos < total_args {
10046 let value = values.get(i).cloned().unwrap_or(JValue::Null);
10047 full_args[pos] = value;
10048 }
10049 }
10050
10051 // Pop lambda scope, then push a new scope for temp args
10052 self.context.pop_scope();
10053 self.context.push_scope();
10054
10055 // Build AST nodes for the function call arguments
10056 let mut temp_args: Vec<AstNode> = Vec::new();
10057 for (i, value) in full_args.iter().enumerate() {
10058 let temp_name = format!("__temp_arg_{}", i);
10059 self.context.bind(temp_name.clone(), value.clone());
10060 temp_args.push(AstNode::Variable(temp_name));
10061 }
10062
10063 // Call the original function
10064 let result =
10065 self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
10066
10067 // Pop temp scope
10068 self.context.pop_scope();
10069
10070 return result;
10071 }
10072 }
10073 }
10074
10075 // Evaluate lambda body (normal case)
10076 // Use captured_data for lexical scoping if available, otherwise use call-site data
10077 let body_data = captured_data.unwrap_or(data);
10078 let result = self.evaluate_internal(body, body_data)?;
10079
10080 // Pop lambda scope, preserving any lambdas referenced by the return value
10081 // Fast path: scalar results can never contain lambda references
10082 let is_scalar = matches!(
10083 &result,
10084 JValue::Number(_)
10085 | JValue::Bool(_)
10086 | JValue::String(_)
10087 | JValue::Null
10088 | JValue::Undefined
10089 );
10090 if is_scalar {
10091 self.context.pop_scope();
10092 } else {
10093 let lambdas_to_keep = self.extract_lambda_ids(&result);
10094 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
10095 }
10096
10097 Ok(result)
10098 }
10099
10100 /// Invoke a lambda with tail call optimization using a trampoline
10101 /// This method uses an iterative loop to handle tail-recursive calls without
10102 /// growing the stack, enabling deep recursion for tail-recursive functions.
10103 fn invoke_lambda_with_tco(
10104 &mut self,
10105 stored_lambda: &StoredLambda,
10106 initial_args: &[JValue],
10107 data: &JValue,
10108 ) -> Result<JValue, EvaluatorError> {
10109 let mut current_lambda = stored_lambda.clone();
10110 let mut current_args = initial_args.to_vec();
10111 let mut current_data = data.clone();
10112
10113 // Maximum number of tail call iterations to prevent infinite loops
10114 // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
10115 const MAX_TCO_ITERATIONS: usize = 100_000;
10116 let mut iterations = 0;
10117
10118 // Push a persistent scope for the TCO trampoline loop.
10119 // This scope persists across all iterations so that lambdas defined
10120 // in one iteration (like recursive $iter) remain available in subsequent ones.
10121 self.context.push_scope();
10122
10123 // Trampoline loop - keeps evaluating until we get a final value
10124 let result = loop {
10125 iterations += 1;
10126 // The hardcoded iteration cap is a backstop for when no timeout is
10127 // configured; it must not preempt a configured timeout (which is the
10128 // more specific, user-controlled guardrail). Without this gate, an
10129 // infinite tail-recursive loop with a cheap per-iteration body hits
10130 // this cap in single-digit-to-tens of milliseconds and reports the
10131 // misleading "U1001: Stack overflow" (TCO does not grow the stack;
10132 // there is no depth-500 stack here) instead of D1012, for *any*
10133 // realistic `timeout_ms` (100ms, 1s, the docs' own 5000ms default) -
10134 // defeating the purpose of the timeout guardrail for exactly the
10135 // scenario it exists to catch (see jsonata-js's own `$inf := function
10136 // (){$inf()}; $inf()` guardrails-documentation example).
10137 if self.options.timeout_ms.is_none() && iterations > MAX_TCO_ITERATIONS {
10138 self.context.pop_scope();
10139 return Err(EvaluatorError::EvaluationError(
10140 "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
10141 ));
10142 }
10143 if let Err(e) = check_loop_timeout(&self.options, self.start_time) {
10144 self.context.pop_scope();
10145 return Err(e);
10146 }
10147
10148 // Evaluate the lambda body within the persistent scope
10149 let result =
10150 self.invoke_lambda_body_for_tco(¤t_lambda, ¤t_args, ¤t_data)?;
10151
10152 match result {
10153 LambdaResult::JValue(v) => break v,
10154 LambdaResult::TailCall { lambda, args, data } => {
10155 // Continue with the tail call - no stack growth
10156 current_lambda = *lambda;
10157 current_args = args;
10158 current_data = data;
10159 }
10160 }
10161 };
10162
10163 // Pop the persistent TCO scope, preserving lambdas referenced by the result
10164 let lambdas_to_keep = self.extract_lambda_ids(&result);
10165 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
10166
10167 Ok(result)
10168 }
10169
10170 /// Evaluate a lambda body, detecting tail calls for TCO
10171 /// Returns either a final value or a tail call continuation.
10172 /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
10173 /// manages the persistent scope for the trampoline loop.
10174 fn invoke_lambda_body_for_tco(
10175 &mut self,
10176 lambda: &StoredLambda,
10177 values: &[JValue],
10178 data: &JValue,
10179 ) -> Result<LambdaResult, EvaluatorError> {
10180 // Validate signature if present
10181 let coerced_values = if let Some(sig_str) = &lambda.signature {
10182 match crate::signature::Signature::parse(sig_str) {
10183 Ok(sig) => match sig.validate_and_coerce(values, data) {
10184 Ok(coerced) => coerced,
10185 Err(e) => match e {
10186 crate::signature::SignatureError::UndefinedArgument => {
10187 return Ok(LambdaResult::JValue(JValue::Null));
10188 }
10189 crate::signature::SignatureError::ArgumentTypeMismatch {
10190 index,
10191 expected,
10192 } => {
10193 return Err(EvaluatorError::TypeError(
10194 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
10195 ));
10196 }
10197 crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
10198 return Err(EvaluatorError::TypeError(format!(
10199 "T0412: Argument {} of function must be an array of {}",
10200 index, expected
10201 )));
10202 }
10203 crate::signature::SignatureError::ContextTypeMismatch {
10204 index,
10205 expected,
10206 } => {
10207 return Err(EvaluatorError::TypeError(format!(
10208 "T0411: Context value at argument {} does not match function signature (expected {})",
10209 index, expected
10210 )));
10211 }
10212 _ => {
10213 return Err(EvaluatorError::TypeError(format!(
10214 "Signature validation failed: {}",
10215 e
10216 )));
10217 }
10218 },
10219 },
10220 Err(e) => {
10221 return Err(EvaluatorError::EvaluationError(format!(
10222 "Invalid signature: {}",
10223 e
10224 )));
10225 }
10226 }
10227 } else {
10228 values.to_vec()
10229 };
10230
10231 // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
10232 // Apply captured environment
10233 for (name, value) in &lambda.captured_env {
10234 self.context.bind(name.clone(), value.clone());
10235 }
10236
10237 // Bind parameters
10238 for (i, param) in lambda.params.iter().enumerate() {
10239 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
10240 self.context.bind(param.clone(), value);
10241 }
10242
10243 // Evaluate the body with tail call detection
10244 let body_data = lambda.captured_data.as_ref().unwrap_or(data);
10245 self.evaluate_for_tco(&lambda.body, body_data)
10246 }
10247
10248 /// Evaluate an expression for TCO, detecting tail calls
10249 /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
10250 fn evaluate_for_tco(
10251 &mut self,
10252 node: &AstNode,
10253 data: &JValue,
10254 ) -> Result<LambdaResult, EvaluatorError> {
10255 match node {
10256 // Conditional: evaluate condition, then evaluate the chosen branch for TCO
10257 AstNode::Conditional {
10258 condition,
10259 then_branch,
10260 else_branch,
10261 } => {
10262 let cond_value = self.evaluate_internal(condition, data)?;
10263 let is_truthy = self.is_truthy(&cond_value);
10264
10265 if is_truthy {
10266 self.evaluate_for_tco(then_branch, data)
10267 } else if let Some(else_expr) = else_branch {
10268 self.evaluate_for_tco(else_expr, data)
10269 } else {
10270 Ok(LambdaResult::JValue(JValue::Null))
10271 }
10272 }
10273
10274 // Block: evaluate all but last normally, last for TCO
10275 AstNode::Block(exprs) => {
10276 if exprs.is_empty() {
10277 return Ok(LambdaResult::JValue(JValue::Null));
10278 }
10279
10280 // Evaluate all expressions except the last
10281 let mut result = JValue::Null;
10282 for (i, expr) in exprs.iter().enumerate() {
10283 if i == exprs.len() - 1 {
10284 // Last expression - evaluate for TCO
10285 return self.evaluate_for_tco(expr, data);
10286 } else {
10287 result = self.evaluate_internal(expr, data)?;
10288 }
10289 }
10290 Ok(LambdaResult::JValue(result))
10291 }
10292
10293 // Variable binding: evaluate value, bind, then evaluate result for TCO if present
10294 AstNode::Binary {
10295 op: BinaryOp::ColonEqual,
10296 lhs,
10297 rhs,
10298 } => {
10299 // This is var := value; get the variable name
10300 let var_name = match lhs.as_ref() {
10301 AstNode::Variable(name) => name.clone(),
10302 _ => {
10303 // Not a simple variable binding, evaluate normally
10304 let result = self.evaluate_internal(node, data)?;
10305 return Ok(LambdaResult::JValue(result));
10306 }
10307 };
10308
10309 // Check if RHS is a lambda - store it specially
10310 if let AstNode::Lambda {
10311 params,
10312 body,
10313 signature,
10314 thunk,
10315 } = rhs.as_ref()
10316 {
10317 let captured_env = self.capture_environment_for(body, params);
10318 let compiled_body = if !thunk {
10319 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
10320 try_compile_expr_with_allowed_vars(body, &var_refs)
10321 } else {
10322 None
10323 };
10324 let stored_lambda = StoredLambda {
10325 params: params.clone(),
10326 body: (**body).clone(),
10327 compiled_body,
10328 signature: signature.clone(),
10329 captured_env,
10330 captured_data: Some(data.clone()),
10331 thunk: *thunk,
10332 };
10333 self.context.bind_lambda(var_name, stored_lambda);
10334 let lambda_repr =
10335 JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
10336 return Ok(LambdaResult::JValue(lambda_repr));
10337 }
10338
10339 // Evaluate the RHS
10340 let value = self.evaluate_internal(rhs, data)?;
10341 self.context.bind(var_name, value.clone());
10342 Ok(LambdaResult::JValue(value))
10343 }
10344
10345 // Function call - this is where TCO happens
10346 AstNode::Function { name, args, .. } => {
10347 // Check if this is a call to a stored lambda (user function)
10348 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
10349 if stored_lambda.thunk {
10350 let mut evaluated_args = Vec::with_capacity(args.len());
10351 for arg in args {
10352 evaluated_args.push(self.evaluate_internal(arg, data)?);
10353 }
10354 return Ok(LambdaResult::TailCall {
10355 lambda: Box::new(stored_lambda),
10356 args: evaluated_args,
10357 data: data.clone(),
10358 });
10359 }
10360 }
10361 // Not a thunk lambda - evaluate normally
10362 let result = self.evaluate_internal(node, data)?;
10363 Ok(LambdaResult::JValue(result))
10364 }
10365
10366 // Call node (calling a lambda value)
10367 AstNode::Call { procedure, args } => {
10368 // Evaluate the procedure to get the callable
10369 let callable = self.evaluate_internal(procedure, data)?;
10370
10371 // Check if it's a lambda with TCO
10372 if let JValue::Lambda { lambda_id, .. } = &callable {
10373 if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
10374 if stored_lambda.thunk {
10375 let mut evaluated_args = Vec::with_capacity(args.len());
10376 for arg in args {
10377 evaluated_args.push(self.evaluate_internal(arg, data)?);
10378 }
10379 return Ok(LambdaResult::TailCall {
10380 lambda: Box::new(stored_lambda),
10381 args: evaluated_args,
10382 data: data.clone(),
10383 });
10384 }
10385 }
10386 }
10387 // Not a thunk - evaluate normally
10388 let result = self.evaluate_internal(node, data)?;
10389 Ok(LambdaResult::JValue(result))
10390 }
10391
10392 // Variable reference that might be a function call
10393 // This handles cases like $f($x) where $f is referenced by name
10394 AstNode::Variable(_) => {
10395 let result = self.evaluate_internal(node, data)?;
10396 Ok(LambdaResult::JValue(result))
10397 }
10398
10399 // Any other expression - evaluate normally
10400 _ => {
10401 let result = self.evaluate_internal(node, data)?;
10402 Ok(LambdaResult::JValue(result))
10403 }
10404 }
10405 }
10406
10407 /// Match with custom matcher function
10408 ///
10409 /// Implements custom matcher support for $match(str, matcherFunction, limit?)
10410 /// The matcher function is called with the string and returns:
10411 /// { match: string, start: number, end: number, groups: [], next: function }
10412 /// The next function is called repeatedly to get subsequent matches
10413 fn match_with_custom_matcher(
10414 &mut self,
10415 str_value: &str,
10416 matcher_node: &AstNode,
10417 limit: Option<usize>,
10418 data: &JValue,
10419 ) -> Result<JValue, EvaluatorError> {
10420 let mut results = Vec::new();
10421 let mut count = 0;
10422
10423 // Call the matcher function with the string
10424 let str_val = JValue::string(str_value.to_string());
10425 let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
10426
10427 // Iterate through matches following the 'next' chain
10428 while !current_match.is_undefined() && !current_match.is_null() {
10429 // Check limit
10430 if let Some(lim) = limit {
10431 if count >= lim {
10432 break;
10433 }
10434 }
10435
10436 // Extract match information from the result object
10437 if let JValue::Object(ref match_obj) = current_match {
10438 // Validate that this is a proper match object
10439 let has_match = match_obj.contains_key("match");
10440 let has_start = match_obj.contains_key("start");
10441 let has_end = match_obj.contains_key("end");
10442 let has_groups = match_obj.contains_key("groups");
10443 let has_next = match_obj.contains_key("next");
10444
10445 if !has_match && !has_start && !has_end && !has_groups && !has_next {
10446 // Invalid matcher result - T1010 error
10447 return Err(EvaluatorError::EvaluationError(
10448 "T1010: The matcher function did not return the correct object structure"
10449 .to_string(),
10450 ));
10451 }
10452
10453 // Build the result match object (match, index, groups)
10454 let mut result_obj = IndexMap::new();
10455
10456 if let Some(match_val) = match_obj.get("match") {
10457 result_obj.insert("match".to_string(), match_val.clone());
10458 }
10459
10460 if let Some(start_val) = match_obj.get("start") {
10461 result_obj.insert("index".to_string(), start_val.clone());
10462 }
10463
10464 if let Some(groups_val) = match_obj.get("groups") {
10465 result_obj.insert("groups".to_string(), groups_val.clone());
10466 }
10467
10468 results.push(JValue::object(result_obj));
10469 count += 1;
10470
10471 // Get the next match by calling the 'next' function
10472 if let Some(next_func) = match_obj.get("next") {
10473 if let Some(stored) = self.lookup_lambda_from_value(next_func) {
10474 current_match = self.invoke_stored_lambda(&stored, &[], data)?;
10475 continue;
10476 }
10477 }
10478
10479 // No next function or couldn't call it - stop iteration
10480 break;
10481 } else {
10482 // Not a valid match object
10483 break;
10484 }
10485 }
10486
10487 // Return results
10488 if results.is_empty() {
10489 Ok(JValue::Undefined)
10490 } else {
10491 Ok(JValue::array(results))
10492 }
10493 }
10494
10495 /// Replace with lambda/function callback
10496 ///
10497 /// Implements lambda replacement for $replace(str, pattern, function, limit?)
10498 /// The function receives a match object with: match, start, end, groups
10499 fn replace_with_lambda(
10500 &mut self,
10501 str_value: &JValue,
10502 pattern_value: &JValue,
10503 lambda_value: &JValue,
10504 limit_value: Option<&JValue>,
10505 data: &JValue,
10506 ) -> Result<JValue, EvaluatorError> {
10507 // Extract string
10508 let s = match str_value {
10509 JValue::String(s) => &**s,
10510 _ => {
10511 return Err(EvaluatorError::TypeError(
10512 "replace() requires string arguments".to_string(),
10513 ))
10514 }
10515 };
10516
10517 // Extract regex pattern
10518 let (pattern, flags) =
10519 crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
10520 EvaluatorError::TypeError(
10521 "replace() pattern must be a regex when using lambda replacement".to_string(),
10522 )
10523 })?;
10524
10525 // Build regex
10526 let re = crate::functions::string::build_regex(&pattern, &flags)?;
10527
10528 // Parse limit
10529 let limit = if let Some(lim_val) = limit_value {
10530 match lim_val {
10531 JValue::Number(n) => {
10532 let lim_f64 = *n;
10533 if lim_f64 < 0.0 {
10534 return Err(EvaluatorError::EvaluationError(format!(
10535 "D3011: Limit must be non-negative, got {}",
10536 lim_f64
10537 )));
10538 }
10539 Some(lim_f64 as usize)
10540 }
10541 _ => {
10542 return Err(EvaluatorError::TypeError(
10543 "replace() limit must be a number".to_string(),
10544 ))
10545 }
10546 }
10547 } else {
10548 None
10549 };
10550
10551 // Iterate through matches and replace using lambda
10552 let mut result = String::new();
10553 let mut last_end = 0;
10554 let mut count = 0;
10555
10556 for cap in re.captures_iter(s) {
10557 // Check limit
10558 if let Some(lim) = limit {
10559 if count >= lim {
10560 break;
10561 }
10562 }
10563
10564 let m = cap.get(0).unwrap();
10565 let match_start = m.start();
10566 let match_end = m.end();
10567 let match_str = m.as_str();
10568
10569 // Add text before match
10570 result.push_str(&s[last_end..match_start]);
10571
10572 // Build match object
10573 let groups: Vec<JValue> = (1..cap.len())
10574 .map(|i| {
10575 cap.get(i)
10576 .map(|m| JValue::string(m.as_str().to_string()))
10577 .unwrap_or(JValue::Null)
10578 })
10579 .collect();
10580
10581 let mut match_map = IndexMap::new();
10582 match_map.insert("match".to_string(), JValue::string(match_str));
10583 match_map.insert("start".to_string(), JValue::Number(match_start as f64));
10584 match_map.insert("end".to_string(), JValue::Number(match_end as f64));
10585 match_map.insert("groups".to_string(), JValue::array(groups));
10586 let match_obj = JValue::object(match_map);
10587
10588 // Invoke lambda with match object
10589 let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
10590 EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
10591 })?;
10592 let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
10593 let replacement_str = match lambda_result {
10594 JValue::String(s) => s,
10595 _ => {
10596 return Err(EvaluatorError::TypeError(format!(
10597 "D3012: Replacement function must return a string, got {:?}",
10598 lambda_result
10599 )))
10600 }
10601 };
10602
10603 // Add replacement
10604 result.push_str(&replacement_str);
10605
10606 last_end = match_end;
10607 count += 1;
10608 }
10609
10610 // Add remaining text after last match
10611 result.push_str(&s[last_end..]);
10612
10613 Ok(JValue::string(result))
10614 }
10615
10616 /// Capture the current environment bindings for closure support
10617 fn capture_current_environment(&self) -> HashMap<String, JValue> {
10618 self.context.all_bindings()
10619 }
10620
10621 /// Capture only the variables referenced by a lambda body (selective capture).
10622 /// This avoids cloning the entire environment when only a few variables are needed.
10623 fn capture_environment_for(
10624 &self,
10625 body: &AstNode,
10626 params: &[String],
10627 ) -> HashMap<String, JValue> {
10628 let free_vars = Self::collect_free_variables(body, params);
10629 if free_vars.is_empty() {
10630 return HashMap::new();
10631 }
10632 let mut result = HashMap::new();
10633 for var_name in &free_vars {
10634 if let Some(value) = self.context.lookup(var_name) {
10635 result.insert(var_name.clone(), value.clone());
10636 }
10637 }
10638 result
10639 }
10640
10641 /// Collect all free variables in an AST node that are not bound by the given params.
10642 /// A "free variable" is one that is referenced but not defined within the expression.
10643 fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
10644 let mut free_vars = HashSet::new();
10645 let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
10646 Self::collect_free_vars_walk(body, &bound, &mut free_vars);
10647 free_vars
10648 }
10649
10650 fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
10651 match node {
10652 AstNode::Variable(name) => {
10653 if !bound.contains(name.as_str()) {
10654 free.insert(name.clone());
10655 }
10656 }
10657 AstNode::Function { name, args, .. } => {
10658 // Function name references a variable (e.g., $f(...))
10659 if !bound.contains(name.as_str()) {
10660 free.insert(name.clone());
10661 }
10662 for arg in args {
10663 Self::collect_free_vars_walk(arg, bound, free);
10664 }
10665 }
10666 AstNode::Lambda { params, body, .. } => {
10667 // Inner lambda introduces new bindings
10668 let mut inner_bound = bound.clone();
10669 for p in params {
10670 inner_bound.insert(p.as_str());
10671 }
10672 Self::collect_free_vars_walk(body, &inner_bound, free);
10673 }
10674 AstNode::Binary { op, lhs, rhs } => {
10675 Self::collect_free_vars_walk(lhs, bound, free);
10676 Self::collect_free_vars_walk(rhs, bound, free);
10677 // For ColonEqual, note: the binding is visible after this expr in blocks,
10678 // but block handling takes care of that separately
10679 let _ = op;
10680 }
10681 AstNode::Unary { operand, .. } => {
10682 Self::collect_free_vars_walk(operand, bound, free);
10683 }
10684 AstNode::Path { steps } => {
10685 for step in steps {
10686 Self::collect_free_vars_walk(&step.node, bound, free);
10687 for stage in &step.stages {
10688 match stage {
10689 Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
10690 // An index stage binds a variable; it introduces no
10691 // free variable references.
10692 Stage::Index(_) => {}
10693 }
10694 }
10695 }
10696 }
10697 AstNode::Call { procedure, args } => {
10698 Self::collect_free_vars_walk(procedure, bound, free);
10699 for arg in args {
10700 Self::collect_free_vars_walk(arg, bound, free);
10701 }
10702 }
10703 AstNode::Conditional {
10704 condition,
10705 then_branch,
10706 else_branch,
10707 } => {
10708 Self::collect_free_vars_walk(condition, bound, free);
10709 Self::collect_free_vars_walk(then_branch, bound, free);
10710 if let Some(else_expr) = else_branch {
10711 Self::collect_free_vars_walk(else_expr, bound, free);
10712 }
10713 }
10714 AstNode::Block(exprs) => {
10715 let mut block_bound = bound.clone();
10716 for expr in exprs {
10717 Self::collect_free_vars_walk(expr, &block_bound, free);
10718 // Bindings introduced via := become bound for subsequent expressions
10719 if let AstNode::Binary {
10720 op: BinaryOp::ColonEqual,
10721 lhs,
10722 ..
10723 } = expr
10724 {
10725 if let AstNode::Variable(var_name) = lhs.as_ref() {
10726 block_bound.insert(var_name.as_str());
10727 }
10728 }
10729 }
10730 }
10731 AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
10732 for expr in exprs {
10733 Self::collect_free_vars_walk(expr, bound, free);
10734 }
10735 }
10736 AstNode::Object(pairs) => {
10737 for (key, value) in pairs {
10738 Self::collect_free_vars_walk(key, bound, free);
10739 Self::collect_free_vars_walk(value, bound, free);
10740 }
10741 }
10742 AstNode::ObjectTransform { input, pattern } => {
10743 Self::collect_free_vars_walk(input, bound, free);
10744 for (key, value) in pattern {
10745 Self::collect_free_vars_walk(key, bound, free);
10746 Self::collect_free_vars_walk(value, bound, free);
10747 }
10748 }
10749 AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
10750 Self::collect_free_vars_walk(expr, bound, free);
10751 }
10752 AstNode::Sort { input, terms } => {
10753 Self::collect_free_vars_walk(input, bound, free);
10754 for (expr, _) in terms {
10755 Self::collect_free_vars_walk(expr, bound, free);
10756 }
10757 }
10758 AstNode::Transform {
10759 location,
10760 update,
10761 delete,
10762 } => {
10763 Self::collect_free_vars_walk(location, bound, free);
10764 Self::collect_free_vars_walk(update, bound, free);
10765 if let Some(del) = delete {
10766 Self::collect_free_vars_walk(del, bound, free);
10767 }
10768 }
10769 // Leaf nodes with no variable references
10770 AstNode::String(_)
10771 | AstNode::Name(_)
10772 | AstNode::Number(_)
10773 | AstNode::Boolean(_)
10774 | AstNode::Null
10775 | AstNode::Undefined
10776 | AstNode::Placeholder
10777 | AstNode::Regex { .. }
10778 | AstNode::Wildcard
10779 | AstNode::Descendant
10780 | AstNode::Parent(_)
10781 | AstNode::ParentVariable(_) => {}
10782 }
10783 }
10784
10785 /// Check if a name refers to a built-in function
10786 fn is_builtin_function(&self, name: &str) -> bool {
10787 matches!(
10788 name,
10789 // String functions
10790 "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
10791 "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
10792 "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
10793 "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
10794
10795 // Numeric functions
10796 "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
10797 "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
10798
10799 // Aggregation functions
10800 "sum" | "max" | "min" | "average" |
10801
10802 // Boolean/logic functions
10803 "boolean" | "not" | "exists" |
10804
10805 // Array functions
10806 "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
10807
10808 // Object functions
10809 "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
10810
10811 // Higher-order functions
10812 "map" | "filter" | "reduce" | "singletonArray" |
10813
10814 // Date/time functions
10815 "now" | "millis" | "fromMillis" | "toMillis"
10816 )
10817 }
10818
10819 /// Call a built-in function directly with pre-evaluated Values
10820 /// This is used when passing built-in functions to higher-order functions like $map
10821 fn call_builtin_with_values(
10822 &mut self,
10823 name: &str,
10824 values: &[JValue],
10825 ) -> Result<JValue, EvaluatorError> {
10826 use crate::functions;
10827
10828 if values.is_empty() {
10829 return Err(EvaluatorError::EvaluationError(format!(
10830 "{}() requires at least 1 argument",
10831 name
10832 )));
10833 }
10834
10835 // Normalize every lazy top-level argument so a conversion failure raises
10836 // TypeError here rather than being swallowed by a downstream function that
10837 // maps `LazyPyDict`/failed-conversion to a misleading result (e.g. `string()`'s
10838 // lazy arm silently stringifying to `"null"`). This is reachable from
10839 // higher-order functions passed a bare builtin reference, e.g.
10840 // `$map(items, $string)` where `items` elements are lazy (first argument),
10841 // or `$f := $append; $f(a, x)` where `x` is a lazy non-first argument.
10842 // Mirrors the blanket normalization `evaluate_function_call` applies to
10843 // ALL `evaluated_args` for inline builtin calls. Only top-level lazy
10844 // values are normalized -- arrays/objects containing lazy elements are
10845 // left as-is (pass-through preservation), same as every other dispatch
10846 // site. Guarded by `is_lazy` so the common (no lazy operand) path pays
10847 // no allocation.
10848 let normalized_values: Vec<JValue>;
10849 let values: &[JValue] = if values.iter().any(|v| v.is_lazy()) {
10850 normalized_values = values
10851 .iter()
10852 .map(|v| {
10853 if v.is_lazy() {
10854 normalize_lazy(v)
10855 } else {
10856 Ok(v.clone())
10857 }
10858 })
10859 .collect::<Result<Vec<_>, _>>()?;
10860 &normalized_values
10861 } else {
10862 values
10863 };
10864 let arg = &values[0];
10865
10866 match name {
10867 "string" => Ok(functions::string::string(arg, None)?),
10868 "number" => Ok(functions::numeric::number(arg)?),
10869 "boolean" => Ok(functions::boolean::boolean(arg)?),
10870 "not" => {
10871 let b = functions::boolean::boolean(arg)?;
10872 match b {
10873 JValue::Bool(val) => Ok(JValue::Bool(!val)),
10874 _ => Err(EvaluatorError::TypeError(
10875 "not() requires a boolean".to_string(),
10876 )),
10877 }
10878 }
10879 "exists" => Ok(JValue::Bool(!arg.is_null())),
10880 "abs" => match arg {
10881 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
10882 _ => Err(EvaluatorError::TypeError(
10883 "abs() requires a number argument".to_string(),
10884 )),
10885 },
10886 "floor" => match arg {
10887 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
10888 _ => Err(EvaluatorError::TypeError(
10889 "floor() requires a number argument".to_string(),
10890 )),
10891 },
10892 "ceil" => match arg {
10893 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
10894 _ => Err(EvaluatorError::TypeError(
10895 "ceil() requires a number argument".to_string(),
10896 )),
10897 },
10898 "round" => match arg {
10899 JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
10900 _ => Err(EvaluatorError::TypeError(
10901 "round() requires a number argument".to_string(),
10902 )),
10903 },
10904 "sqrt" => match arg {
10905 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
10906 _ => Err(EvaluatorError::TypeError(
10907 "sqrt() requires a number argument".to_string(),
10908 )),
10909 },
10910 "uppercase" => match arg {
10911 JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
10912 JValue::Null => Ok(JValue::Null),
10913 _ => Err(EvaluatorError::TypeError(
10914 "uppercase() requires a string argument".to_string(),
10915 )),
10916 },
10917 "lowercase" => match arg {
10918 JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
10919 JValue::Null => Ok(JValue::Null),
10920 _ => Err(EvaluatorError::TypeError(
10921 "lowercase() requires a string argument".to_string(),
10922 )),
10923 },
10924 "trim" => match arg {
10925 JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
10926 JValue::Null => Ok(JValue::Null),
10927 _ => Err(EvaluatorError::TypeError(
10928 "trim() requires a string argument".to_string(),
10929 )),
10930 },
10931 "length" => match arg {
10932 JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
10933 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10934 JValue::Null => Ok(JValue::Null),
10935 _ => Err(EvaluatorError::TypeError(
10936 "length() requires a string or array argument".to_string(),
10937 )),
10938 },
10939 "sum" => match arg {
10940 JValue::Array(arr) => {
10941 let mut total = 0.0;
10942 for item in arr.iter() {
10943 match item {
10944 JValue::Number(n) => {
10945 total += *n;
10946 }
10947 _ => {
10948 return Err(EvaluatorError::TypeError(
10949 "sum() requires all array elements to be numbers".to_string(),
10950 ));
10951 }
10952 }
10953 }
10954 Ok(JValue::Number(total))
10955 }
10956 JValue::Number(n) => Ok(JValue::Number(*n)),
10957 JValue::Null => Ok(JValue::Null),
10958 _ => Err(EvaluatorError::TypeError(
10959 "sum() requires an array of numbers".to_string(),
10960 )),
10961 },
10962 "count" => {
10963 match arg {
10964 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10965 JValue::Null => Ok(JValue::Number(0.0)),
10966 _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10967 }
10968 }
10969 "max" => match arg {
10970 JValue::Array(arr) => {
10971 let mut max_val: Option<f64> = None;
10972 for item in arr.iter() {
10973 if let JValue::Number(n) = item {
10974 let f = *n;
10975 max_val = Some(max_val.map_or(f, |m| m.max(f)));
10976 }
10977 }
10978 max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10979 }
10980 JValue::Number(n) => Ok(JValue::Number(*n)),
10981 JValue::Null => Ok(JValue::Null),
10982 _ => Err(EvaluatorError::TypeError(
10983 "max() requires an array of numbers".to_string(),
10984 )),
10985 },
10986 "min" => match arg {
10987 JValue::Array(arr) => {
10988 let mut min_val: Option<f64> = None;
10989 for item in arr.iter() {
10990 if let JValue::Number(n) = item {
10991 let f = *n;
10992 min_val = Some(min_val.map_or(f, |m| m.min(f)));
10993 }
10994 }
10995 min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10996 }
10997 JValue::Number(n) => Ok(JValue::Number(*n)),
10998 JValue::Null => Ok(JValue::Null),
10999 _ => Err(EvaluatorError::TypeError(
11000 "min() requires an array of numbers".to_string(),
11001 )),
11002 },
11003 "average" => match arg {
11004 JValue::Array(arr) => {
11005 let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
11006 if nums.is_empty() {
11007 Ok(JValue::Null)
11008 } else {
11009 let avg = nums.iter().sum::<f64>() / nums.len() as f64;
11010 Ok(JValue::Number(avg))
11011 }
11012 }
11013 JValue::Number(n) => Ok(JValue::Number(*n)),
11014 JValue::Null => Ok(JValue::Null),
11015 _ => Err(EvaluatorError::TypeError(
11016 "average() requires an array of numbers".to_string(),
11017 )),
11018 },
11019 "append" => {
11020 // append(array1, array2) - append second array to first
11021 if values.len() < 2 {
11022 return Err(EvaluatorError::EvaluationError(
11023 "append() requires 2 arguments".to_string(),
11024 ));
11025 }
11026 let first = &values[0];
11027 let second = &values[1];
11028
11029 // Convert first to array if needed
11030 let mut result = match first {
11031 JValue::Array(arr) => arr.to_vec(),
11032 JValue::Null => vec![],
11033 other => vec![other.clone()],
11034 };
11035
11036 // Append second (flatten if array)
11037 match second {
11038 JValue::Array(arr) => result.extend(arr.iter().cloned()),
11039 JValue::Null => {}
11040 other => result.push(other.clone()),
11041 }
11042
11043 check_sequence_length(result.len(), &self.options)?;
11044 Ok(JValue::array(result))
11045 }
11046 "reverse" => match arg {
11047 JValue::Array(arr) => {
11048 let mut reversed = arr.to_vec();
11049 reversed.reverse();
11050 Ok(JValue::array(reversed))
11051 }
11052 JValue::Null => Ok(JValue::Null),
11053 _ => Err(EvaluatorError::TypeError(
11054 "reverse() requires an array".to_string(),
11055 )),
11056 },
11057 "keys" => match arg {
11058 JValue::Object(obj) => {
11059 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
11060 check_sequence_length(keys.len(), &self.options)?;
11061 Ok(JValue::array(keys))
11062 }
11063 JValue::Null => Ok(JValue::Null),
11064 _ => Err(EvaluatorError::TypeError(
11065 "keys() requires an object".to_string(),
11066 )),
11067 },
11068
11069 // Add more functions as needed
11070 _ => Err(EvaluatorError::ReferenceError(format!(
11071 "Built-in function {} cannot be called with values directly",
11072 name
11073 ))),
11074 }
11075 }
11076
11077 /// Collect all descendant values recursively
11078 fn collect_descendants(&self, value: &JValue) -> Result<Vec<JValue>, EvaluatorError> {
11079 let mut descendants = Vec::new();
11080
11081 match value {
11082 JValue::Null => {
11083 // Null has no descendants, return empty
11084 return Ok(descendants);
11085 }
11086 JValue::Object(obj) => {
11087 // Include the current object
11088 descendants.push(value.clone());
11089
11090 for val in obj.values() {
11091 // Recursively collect descendants
11092 descendants.extend(self.collect_descendants(val)?);
11093 }
11094 }
11095 #[cfg(feature = "python")]
11096 JValue::LazyPyDict(lazy) => {
11097 // Include the current (lazy) object, mirroring the Object arm
11098 descendants.push(value.clone());
11099
11100 let obj = lazy.to_object().map_err(EvaluatorError::from)?;
11101 for val in obj.values() {
11102 descendants.extend(self.collect_descendants(val)?);
11103 }
11104 }
11105 JValue::Array(arr) => {
11106 // DO NOT include the array itself - only recurse into elements
11107 // This matches JavaScript behavior: arrays are traversed but not collected
11108 for val in arr.iter() {
11109 // Recursively collect descendants
11110 descendants.extend(self.collect_descendants(val)?);
11111 }
11112 }
11113 _ => {
11114 // For primitives (string, number, boolean), just include the value itself
11115 descendants.push(value.clone());
11116 }
11117 }
11118
11119 Ok(descendants)
11120 }
11121
11122 /// Evaluate a predicate (array filter or index)
11123 fn evaluate_predicate(
11124 &mut self,
11125 current: &JValue,
11126 predicate: &AstNode,
11127 ) -> Result<JValue, EvaluatorError> {
11128 // Special case: empty brackets [] (represented as Boolean(true))
11129 // This forces the value to be wrapped in an array
11130 if matches!(predicate, AstNode::Boolean(true)) {
11131 return match current {
11132 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
11133 JValue::Null => Ok(JValue::Null),
11134 other => Ok(JValue::array(vec![other.clone()])),
11135 };
11136 }
11137
11138 match current {
11139 JValue::Array(_arr) => {
11140 // Standalone predicates do simple array operations (no mapping over sub-arrays)
11141
11142 // First, try to evaluate predicate as a simple number (array index)
11143 if let AstNode::Number(n) = predicate {
11144 // Direct array indexing
11145 return self.array_index(current, &JValue::Number(*n));
11146 }
11147
11148 // Fast path: if predicate is definitely a filter expression (comparison/logical),
11149 // skip speculative numeric evaluation and go directly to filter logic
11150 if Self::is_filter_predicate(predicate) {
11151 // Try CompiledExpr fast path
11152 if let Some(compiled) = try_compile_expr(predicate) {
11153 let shape = _arr.first().and_then(build_shape_cache);
11154 let mut filtered = Vec::with_capacity(_arr.len());
11155 for item in _arr.iter() {
11156 let result = if let Some(ref s) = shape {
11157 eval_compiled_shaped(
11158 &compiled,
11159 item,
11160 None,
11161 s,
11162 &self.options,
11163 self.start_time,
11164 )?
11165 } else {
11166 eval_compiled(
11167 &compiled,
11168 item,
11169 None,
11170 &self.options,
11171 self.start_time,
11172 )?
11173 };
11174 if compiled_is_truthy(&result) {
11175 filtered.push(item.clone());
11176 }
11177 }
11178 return Ok(JValue::array(filtered));
11179 }
11180 // Fallback: full AST evaluation per element
11181 let mut filtered = Vec::new();
11182 for item in _arr.iter() {
11183 let item_result = self.evaluate_internal(predicate, item)?;
11184 if self.is_truthy(&item_result) {
11185 filtered.push(item.clone());
11186 }
11187 }
11188 return Ok(JValue::array(filtered));
11189 }
11190
11191 // Try to evaluate the predicate to see if it's a numeric index
11192 // If evaluation succeeds and yields a number, use it as an index
11193 // If evaluation fails (e.g., comparison error), treat as filter
11194 match self.evaluate_internal(predicate, current) {
11195 Ok(JValue::Number(_)) => {
11196 // It's a numeric index
11197 let pred_result = self.evaluate_internal(predicate, current)?;
11198 return self.array_index(current, &pred_result);
11199 }
11200 Ok(JValue::Array(indices)) => {
11201 // Multiple array selectors [[indices]]
11202 // Check if array contains any non-numeric values
11203 let has_non_numeric =
11204 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
11205
11206 if has_non_numeric {
11207 // If array contains non-numeric values, return entire array
11208 return Ok(current.clone());
11209 }
11210
11211 // Collect numeric indices, handling negative indices
11212 let arr_len = _arr.len() as i64;
11213 let mut resolved_indices: Vec<i64> = indices
11214 .iter()
11215 .filter_map(|v| {
11216 if let JValue::Number(n) = v {
11217 let idx = *n as i64;
11218 // Resolve negative indices
11219 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
11220 // Only include valid indices
11221 if actual_idx >= 0 && actual_idx < arr_len {
11222 Some(actual_idx)
11223 } else {
11224 None
11225 }
11226 } else {
11227 None
11228 }
11229 })
11230 .collect();
11231
11232 // Sort and deduplicate indices
11233 resolved_indices.sort();
11234 resolved_indices.dedup();
11235
11236 // Select elements at each sorted index
11237 let result: Vec<JValue> = resolved_indices
11238 .iter()
11239 .map(|&idx| _arr[idx as usize].clone())
11240 .collect();
11241
11242 return Ok(JValue::array(result));
11243 }
11244 Ok(_) => {
11245 // Evaluated successfully but not a number - might be a filter
11246 // Fall through to filter logic
11247 }
11248 Err(_) => {
11249 // Evaluation failed - it's likely a filter expression
11250 // Fall through to filter logic
11251 }
11252 }
11253
11254 // Try CompiledExpr fast path for filter expressions
11255 if let Some(compiled) = try_compile_expr(predicate) {
11256 let shape = _arr.first().and_then(build_shape_cache);
11257 let mut filtered = Vec::with_capacity(_arr.len());
11258 for item in _arr.iter() {
11259 let result = if let Some(ref s) = shape {
11260 eval_compiled_shaped(
11261 &compiled,
11262 item,
11263 None,
11264 s,
11265 &self.options,
11266 self.start_time,
11267 )?
11268 } else {
11269 eval_compiled(&compiled, item, None, &self.options, self.start_time)?
11270 };
11271 if compiled_is_truthy(&result) {
11272 filtered.push(item.clone());
11273 }
11274 }
11275 return Ok(JValue::array(filtered));
11276 }
11277
11278 // It's a filter expression - evaluate the predicate for each array element
11279 let mut filtered = Vec::new();
11280 for item in _arr.iter() {
11281 let item_result = self.evaluate_internal(predicate, item)?;
11282
11283 // If result is truthy, include this item
11284 if self.is_truthy(&item_result) {
11285 filtered.push(item.clone());
11286 }
11287 }
11288
11289 Ok(JValue::array(filtered))
11290 }
11291 JValue::Object(obj) => {
11292 // For objects, predicate can be either:
11293 // 1. A string - property access (computed property name)
11294 // 2. A boolean expression - filter (return object if truthy)
11295 let pred_result = self.evaluate_internal(predicate, current)?;
11296
11297 // If it's a string, use it as a key for property access
11298 if let JValue::String(key) = &pred_result {
11299 return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
11300 }
11301
11302 // Otherwise, treat as a filter expression
11303 // If the predicate is truthy, return the object; otherwise return undefined
11304 if self.is_truthy(&pred_result) {
11305 Ok(current.clone())
11306 } else {
11307 Ok(JValue::Undefined)
11308 }
11309 }
11310 _ => {
11311 // For primitive values (string, number, boolean):
11312 // In JSONata, scalars are treated as single-element arrays when indexed.
11313 // So value[0] returns value, value[1] returns undefined.
11314
11315 // First check if predicate is a numeric literal
11316 if let AstNode::Number(n) = predicate {
11317 // For scalars, index 0 or -1 returns the value, others return undefined
11318 let idx = n.floor() as i64;
11319 if idx == 0 || idx == -1 {
11320 return Ok(current.clone());
11321 } else {
11322 return Ok(JValue::Undefined);
11323 }
11324 }
11325
11326 // Try to evaluate the predicate to see if it's a numeric index
11327 let pred_result = self.evaluate_internal(predicate, current)?;
11328
11329 if let JValue::Number(n) = &pred_result {
11330 // It's a numeric index - treat scalar as single-element array
11331 let idx = n.floor() as i64;
11332 if idx == 0 || idx == -1 {
11333 return Ok(current.clone());
11334 } else {
11335 return Ok(JValue::Undefined);
11336 }
11337 }
11338
11339 // For non-numeric predicates, treat as a filter:
11340 // value[true] returns value, value[false] returns undefined
11341 // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
11342 if self.is_truthy(&pred_result) {
11343 Ok(current.clone())
11344 } else {
11345 // Return undefined (not null) so $map can filter it out
11346 Ok(JValue::Undefined)
11347 }
11348 }
11349 }
11350 }
11351
11352 /// Evaluate a sort term expression, distinguishing missing fields from explicit null
11353 /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
11354 fn evaluate_sort_term(
11355 &mut self,
11356 term_expr: &AstNode,
11357 element: &JValue,
11358 ) -> Result<JValue, EvaluatorError> {
11359 // For tuples (from index binding), extract the actual value from @ field
11360 let actual_element = if let JValue::Object(obj) = element {
11361 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
11362 obj.get("@").cloned().unwrap_or(JValue::Null)
11363 } else {
11364 element.clone()
11365 }
11366 } else {
11367 element.clone()
11368 };
11369
11370 // For simple field access (Path with single Name step), check if field exists
11371 if let AstNode::Path { steps } = term_expr {
11372 if steps.len() == 1 && steps[0].stages.is_empty() {
11373 if let AstNode::Name(field_name) = &steps[0].node {
11374 // Check if the field exists in the element
11375 match &actual_element {
11376 JValue::Object(obj) => {
11377 return match obj.get(field_name) {
11378 Some(val) => Ok(val.clone()), // Field exists (may be null)
11379 None => Ok(JValue::Undefined), // Field is missing
11380 };
11381 }
11382 #[cfg(feature = "python")]
11383 JValue::LazyPyDict(lazy) => {
11384 return Ok(lazy.get_field(field_name)?);
11385 }
11386 _ => return Ok(JValue::Undefined),
11387 }
11388 }
11389 }
11390 }
11391
11392 // For complex expressions, evaluate against the tuple's `@` value (the
11393 // real element), not the wrapper. The tuple's carried focus/index/ancestor
11394 // bindings are reachable via context (bound by evaluate_sort), so a term
11395 // like `$`, `%.Price`, or `$pos` still resolves correctly.
11396 let result = self.evaluate_internal(term_expr, &actual_element)?;
11397
11398 // If the result is null from a complex expression, we can't easily tell if it's
11399 // "missing field" or "explicit null". For now, treat null results as undefined
11400 // to maintain compatibility with existing tests.
11401 // TODO: For full JS compatibility, would need deeper analysis of the expression
11402 if result.is_null() {
11403 return Ok(JValue::Undefined);
11404 }
11405
11406 Ok(result)
11407 }
11408
11409 /// Evaluate sort operator
11410 fn evaluate_sort(
11411 &mut self,
11412 data: &JValue,
11413 terms: &[(AstNode, bool)],
11414 ) -> Result<JValue, EvaluatorError> {
11415 // If data is null, return null
11416 if data.is_null() {
11417 return Ok(JValue::Null);
11418 }
11419
11420 // If data is not an array, return it as-is (can't sort a single value)
11421 let array = match data {
11422 JValue::Array(arr) => arr.clone(),
11423 other => return Ok(other.clone()),
11424 };
11425
11426 // If empty array, return as-is
11427 if array.is_empty() {
11428 return Ok(JValue::Array(array));
11429 }
11430
11431 // Evaluate sort keys for each element
11432 let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
11433
11434 for (idx, element) in array.iter().enumerate() {
11435 let mut sort_keys = Vec::new();
11436
11437 // When sorting a tuple stream (the input path had a `%`/`@`/`#`
11438 // step, so each element is a `{@, !label, $var, __tuple__}`
11439 // wrapper), bind its carried ancestor/focus/index keys into scope
11440 // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
11441 // create_tuple_stream's per-tuple frame binding. Sort terms attach
11442 // to a synthetic step after the last input step, so `%` refers to
11443 // the last input step's ancestry, carried under `!label` here.
11444 // Saves/restores rather than blindly unbinding, so a tuple key
11445 // that collides with a live outer `:=` binding doesn't get
11446 // deleted once this row's sort terms are evaluated.
11447 let tuple_bindings = match element {
11448 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11449 Some(self.bind_tuple_keys(obj))
11450 }
11451 _ => None,
11452 };
11453
11454 // When sorting a tuple stream, `$` and the term's data context are the
11455 // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
11456 // otherwise a term like `^($)` would try to order by the wrapper
11457 // object and raise T2008. The carried focus/index/ancestor keys stay
11458 // reachable via the context bindings established just above.
11459 let term_data = match element {
11460 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
11461 obj.get("@").cloned().unwrap_or(JValue::Null)
11462 }
11463 other => other.clone(),
11464 };
11465
11466 // Evaluate each sort term with $ bound to the element
11467 for (term_expr, _ascending) in terms {
11468 // Save current $ binding
11469 let saved_dollar = self.context.lookup("$").cloned();
11470
11471 // Bind $ to current element
11472 self.context.bind("$".to_string(), term_data.clone());
11473
11474 // Evaluate the sort expression, distinguishing missing fields from explicit null
11475 let sort_value = self.evaluate_sort_term(term_expr, element)?;
11476
11477 // Restore $ binding
11478 if let Some(val) = saved_dollar {
11479 self.context.bind("$".to_string(), val);
11480 } else {
11481 self.context.unbind("$");
11482 }
11483
11484 sort_keys.push(sort_value);
11485 }
11486
11487 if let Some(tuple_bindings) = tuple_bindings {
11488 tuple_bindings.restore(self);
11489 }
11490
11491 indexed_array.push((idx, sort_keys));
11492 }
11493
11494 // Validate that all sort keys are comparable (same type, or undefined)
11495 // Undefined values (missing fields) are allowed and sort to the end
11496 // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
11497 for term_idx in 0..terms.len() {
11498 let mut first_valid_type: Option<&str> = None;
11499
11500 for (_idx, sort_keys) in &indexed_array {
11501 let sort_value = &sort_keys[term_idx];
11502
11503 // Skip undefined markers (missing fields) - these are allowed and sort to end
11504 if sort_value.is_undefined() {
11505 continue;
11506 }
11507
11508 // Get the type name for this value
11509 // Note: explicit null is NOT allowed - typeof null === 'object' in JS
11510 let value_type = match sort_value {
11511 JValue::Number(_) => "number",
11512 JValue::String(_) => "string",
11513 JValue::Bool(_) => "boolean",
11514 JValue::Array(_) => "array",
11515 JValue::Object(_) => "object", // This catches non-undefined objects
11516 JValue::Null => "null", // Explicit null from data
11517 #[cfg(feature = "python")]
11518 JValue::LazyPyDict(_) => "object",
11519 _ => "unknown",
11520 };
11521
11522 // Check that sort keys are only numbers or strings
11523 // Null, boolean, array, and object types are not valid for sorting
11524 if value_type != "number" && value_type != "string" {
11525 return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
11526 }
11527
11528 // Check if this matches the first valid type we saw
11529 if let Some(first_type) = first_valid_type {
11530 if first_type != value_type {
11531 return Err(EvaluatorError::TypeError(format!(
11532 "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
11533 first_type, value_type
11534 )));
11535 }
11536 } else {
11537 first_valid_type = Some(value_type);
11538 }
11539 }
11540 }
11541
11542 // Sort the indexed array
11543 indexed_array.sort_by(|a, b| {
11544 // Compare sort keys in order
11545 for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
11546 let left = &a.1[i];
11547 let right = &b.1[i];
11548
11549 let cmp = self.compare_values(left, right);
11550
11551 if cmp != std::cmp::Ordering::Equal {
11552 return if *ascending { cmp } else { cmp.reverse() };
11553 }
11554 }
11555
11556 // If all keys are equal, maintain original order (stable sort)
11557 a.0.cmp(&b.0)
11558 });
11559
11560 // Extract sorted elements
11561 let sorted: Vec<JValue> = indexed_array
11562 .iter()
11563 .map(|(idx, _)| array[*idx].clone())
11564 .collect();
11565
11566 Ok(JValue::array(sorted))
11567 }
11568
11569 /// Compare two values for sorting (JSONata semantics)
11570 fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
11571 // Handle undefined markers first - they sort to the end
11572 let left_undef = left.is_undefined();
11573 let right_undef = right.is_undefined();
11574
11575 if left_undef && right_undef {
11576 return Ordering::Equal;
11577 }
11578 if left_undef {
11579 return Ordering::Greater; // Undefined sorts last
11580 }
11581 if right_undef {
11582 return Ordering::Less;
11583 }
11584
11585 match (left, right) {
11586 // Nulls also sort last (explicit null in data)
11587 (JValue::Null, JValue::Null) => Ordering::Equal,
11588 (JValue::Null, _) => Ordering::Greater,
11589 (_, JValue::Null) => Ordering::Less,
11590
11591 // Numbers
11592 (JValue::Number(a), JValue::Number(b)) => {
11593 let a_f64 = *a;
11594 let b_f64 = *b;
11595 a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
11596 }
11597
11598 // Strings
11599 (JValue::String(a), JValue::String(b)) => a.cmp(b),
11600
11601 // Booleans
11602 (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
11603
11604 // Arrays (lexicographic comparison)
11605 (JValue::Array(a), JValue::Array(b)) => {
11606 for (a_elem, b_elem) in a.iter().zip(b.iter()) {
11607 let cmp = self.compare_values(a_elem, b_elem);
11608 if cmp != Ordering::Equal {
11609 return cmp;
11610 }
11611 }
11612 a.len().cmp(&b.len())
11613 }
11614
11615 // Different types: use type ordering
11616 // null < bool < number < string < array < object
11617 (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
11618 (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
11619 (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
11620 (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
11621 #[cfg(feature = "python")]
11622 (JValue::Bool(_), JValue::LazyPyDict(_)) => Ordering::Less,
11623
11624 (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
11625 (JValue::Number(_), JValue::String(_)) => Ordering::Less,
11626 (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
11627 (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
11628 #[cfg(feature = "python")]
11629 (JValue::Number(_), JValue::LazyPyDict(_)) => Ordering::Less,
11630
11631 (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
11632 (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
11633 (JValue::String(_), JValue::Array(_)) => Ordering::Less,
11634 (JValue::String(_), JValue::Object(_)) => Ordering::Less,
11635 #[cfg(feature = "python")]
11636 (JValue::String(_), JValue::LazyPyDict(_)) => Ordering::Less,
11637
11638 (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
11639 (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
11640 (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
11641 (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
11642 #[cfg(feature = "python")]
11643 (JValue::Array(_), JValue::LazyPyDict(_)) => Ordering::Less,
11644
11645 (JValue::Object(_), _) => Ordering::Greater,
11646 #[cfg(feature = "python")]
11647 (JValue::LazyPyDict(_), _) => Ordering::Greater,
11648 _ => Ordering::Equal,
11649 }
11650 }
11651
11652 /// Check if a value is truthy (JSONata semantics).
11653 fn is_truthy(&self, value: &JValue) -> bool {
11654 match value {
11655 JValue::Null | JValue::Undefined => false,
11656 JValue::Bool(b) => *b,
11657 JValue::Number(n) => *n != 0.0,
11658 JValue::String(s) => !s.is_empty(),
11659 JValue::Array(arr) => !arr.is_empty(),
11660 JValue::Object(obj) => !obj.is_empty(),
11661 #[cfg(feature = "python")]
11662 JValue::LazyPyDict(lazy) => !lazy.is_empty(),
11663 _ => false,
11664 }
11665 }
11666
11667 /// Check if a value is truthy for the default operator (?:)
11668 /// This has special semantics:
11669 /// - Lambda/function objects are not values, so they're falsy
11670 /// - Arrays containing only falsy elements are falsy
11671 /// - Otherwise, use standard truthiness
11672 fn is_truthy_for_default(&self, value: &JValue) -> bool {
11673 match value {
11674 // Lambda/function values are not data values, so they're falsy
11675 JValue::Lambda { .. } | JValue::Builtin { .. } => false,
11676 // Arrays need special handling - check if all elements are falsy
11677 JValue::Array(arr) => {
11678 if arr.is_empty() {
11679 return false;
11680 }
11681 // Array is truthy only if it contains at least one truthy element
11682 arr.iter().any(|elem| self.is_truthy(elem))
11683 }
11684 // For all other types, use standard truthiness
11685 _ => self.is_truthy(value),
11686 }
11687 }
11688
11689 /// Unwrap singleton arrays to scalar values
11690 /// This is used when no explicit array-keeping operation (like []) was used
11691 fn unwrap_singleton(&self, value: JValue) -> JValue {
11692 match value {
11693 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
11694 _ => value,
11695 }
11696 }
11697
11698 /// Extract lambda IDs from a value (used for closure preservation)
11699 /// Finds any lambda_id references in the value so they can be preserved
11700 /// when exiting a block scope
11701 fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
11702 // Fast path: scalars can never contain lambda references
11703 match value {
11704 JValue::Number(_)
11705 | JValue::Bool(_)
11706 | JValue::String(_)
11707 | JValue::Null
11708 | JValue::Undefined
11709 | JValue::Regex { .. }
11710 | JValue::Builtin { .. } => return Vec::new(),
11711 _ => {}
11712 }
11713 let mut ids = Vec::new();
11714 self.collect_lambda_ids(value, &mut ids);
11715 ids
11716 }
11717
11718 fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
11719 match value {
11720 JValue::Lambda { lambda_id, .. } => {
11721 let id_str = lambda_id.to_string();
11722 if !ids.contains(&id_str) {
11723 ids.push(id_str);
11724 // Transitively follow the stored lambda's captured_env
11725 // to find all referenced lambdas. This is critical for
11726 // closures like the Y-combinator where returned lambdas
11727 // capture other lambdas in their environment.
11728 if let Some(stored) = self.context.lookup_lambda(lambda_id) {
11729 let env_values: Vec<JValue> =
11730 stored.captured_env.values().cloned().collect();
11731 for env_value in &env_values {
11732 self.collect_lambda_ids(env_value, ids);
11733 }
11734 }
11735 }
11736 }
11737 JValue::Object(map) => {
11738 // Recurse into object values
11739 for v in map.values() {
11740 self.collect_lambda_ids(v, ids);
11741 }
11742 }
11743 JValue::Array(arr) => {
11744 // Recurse into array elements
11745 for v in arr.iter() {
11746 self.collect_lambda_ids(v, ids);
11747 }
11748 }
11749 _ => {}
11750 }
11751 }
11752
11753 /// Equality comparison (JSONata semantics)
11754 fn equals(&self, left: &JValue, right: &JValue) -> bool {
11755 crate::functions::array::values_equal(left, right)
11756 }
11757
11758 /// Addition
11759 fn add(
11760 &self,
11761 left: &JValue,
11762 right: &JValue,
11763 left_is_explicit_null: bool,
11764 right_is_explicit_null: bool,
11765 ) -> Result<JValue, EvaluatorError> {
11766 match (left, right) {
11767 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
11768 // Explicit null literal with number -> T2002 error
11769 (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
11770 Err(EvaluatorError::TypeError(
11771 "T2002: The left side of the + operator must evaluate to a number".to_string(),
11772 ))
11773 }
11774 (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
11775 Err(EvaluatorError::TypeError(
11776 "T2002: The right side of the + operator must evaluate to a number".to_string(),
11777 ))
11778 }
11779 (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
11780 Err(EvaluatorError::TypeError(
11781 "T2002: The left side of the + operator must evaluate to a number".to_string(),
11782 ))
11783 }
11784 // Undefined variable (null/undefined) with number -> undefined result
11785 (JValue::Null | JValue::Undefined, JValue::Number(_))
11786 | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
11787 // Boolean with anything (including undefined) -> T2001 error
11788 (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
11789 "T2001: The left side of the '+' operator must evaluate to a number or a string"
11790 .to_string(),
11791 )),
11792 (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
11793 "T2001: The right side of the '+' operator must evaluate to a number or a string"
11794 .to_string(),
11795 )),
11796 // Undefined with undefined -> undefined
11797 (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
11798 Ok(JValue::Null)
11799 }
11800 _ => Err(EvaluatorError::TypeError(format!(
11801 "Cannot add {:?} and {:?}",
11802 left, right
11803 ))),
11804 }
11805 }
11806
11807 /// Subtraction
11808 fn subtract(
11809 &self,
11810 left: &JValue,
11811 right: &JValue,
11812 left_is_explicit_null: bool,
11813 right_is_explicit_null: bool,
11814 ) -> Result<JValue, EvaluatorError> {
11815 match (left, right) {
11816 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
11817 // Explicit null literal -> error
11818 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11819 "T2002: The left side of the - operator must evaluate to a number".to_string(),
11820 )),
11821 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11822 "T2002: The right side of the - operator must evaluate to a number".to_string(),
11823 )),
11824 // Undefined variables -> undefined result
11825 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11826 Ok(JValue::Null)
11827 }
11828 _ => Err(EvaluatorError::TypeError(format!(
11829 "Cannot subtract {:?} and {:?}",
11830 left, right
11831 ))),
11832 }
11833 }
11834
11835 /// Multiplication
11836 fn multiply(
11837 &self,
11838 left: &JValue,
11839 right: &JValue,
11840 left_is_explicit_null: bool,
11841 right_is_explicit_null: bool,
11842 ) -> Result<JValue, EvaluatorError> {
11843 match (left, right) {
11844 (JValue::Number(a), JValue::Number(b)) => {
11845 let result = *a * *b;
11846 // Check for overflow to Infinity
11847 if result.is_infinite() {
11848 return Err(EvaluatorError::EvaluationError(
11849 "D1001: Number out of range".to_string(),
11850 ));
11851 }
11852 Ok(JValue::Number(result))
11853 }
11854 // Explicit null literal -> error
11855 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11856 "T2002: The left side of the * operator must evaluate to a number".to_string(),
11857 )),
11858 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11859 "T2002: The right side of the * operator must evaluate to a number".to_string(),
11860 )),
11861 // Undefined variables -> undefined result
11862 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11863 Ok(JValue::Null)
11864 }
11865 _ => Err(EvaluatorError::TypeError(format!(
11866 "Cannot multiply {:?} and {:?}",
11867 left, right
11868 ))),
11869 }
11870 }
11871
11872 /// Division
11873 fn divide(
11874 &self,
11875 left: &JValue,
11876 right: &JValue,
11877 left_is_explicit_null: bool,
11878 right_is_explicit_null: bool,
11879 ) -> Result<JValue, EvaluatorError> {
11880 match (left, right) {
11881 (JValue::Number(a), JValue::Number(b)) => {
11882 let denominator = *b;
11883 if denominator == 0.0 {
11884 return Err(EvaluatorError::EvaluationError(
11885 "Division by zero".to_string(),
11886 ));
11887 }
11888 Ok(JValue::Number(*a / denominator))
11889 }
11890 // Explicit null literal -> error
11891 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11892 "T2002: The left side of the / operator must evaluate to a number".to_string(),
11893 )),
11894 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11895 "T2002: The right side of the / operator must evaluate to a number".to_string(),
11896 )),
11897 // Undefined variables -> undefined result
11898 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11899 Ok(JValue::Null)
11900 }
11901 _ => Err(EvaluatorError::TypeError(format!(
11902 "Cannot divide {:?} and {:?}",
11903 left, right
11904 ))),
11905 }
11906 }
11907
11908 /// Modulo
11909 fn modulo(
11910 &self,
11911 left: &JValue,
11912 right: &JValue,
11913 left_is_explicit_null: bool,
11914 right_is_explicit_null: bool,
11915 ) -> Result<JValue, EvaluatorError> {
11916 match (left, right) {
11917 (JValue::Number(a), JValue::Number(b)) => {
11918 let denominator = *b;
11919 if denominator == 0.0 {
11920 return Err(EvaluatorError::EvaluationError(
11921 "Division by zero".to_string(),
11922 ));
11923 }
11924 Ok(JValue::Number(*a % denominator))
11925 }
11926 // Explicit null literal -> error
11927 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
11928 "T2002: The left side of the % operator must evaluate to a number".to_string(),
11929 )),
11930 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
11931 "T2002: The right side of the % operator must evaluate to a number".to_string(),
11932 )),
11933 // Undefined variables -> undefined result
11934 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
11935 Ok(JValue::Null)
11936 }
11937 _ => Err(EvaluatorError::TypeError(format!(
11938 "Cannot compute modulo of {:?} and {:?}",
11939 left, right
11940 ))),
11941 }
11942 }
11943
11944 /// Get human-readable type name for error messages
11945 fn type_name(value: &JValue) -> &'static str {
11946 match value {
11947 JValue::Null => "null",
11948 JValue::Bool(_) => "boolean",
11949 JValue::Number(_) => "number",
11950 JValue::String(_) => "string",
11951 JValue::Array(_) => "array",
11952 JValue::Object(_) => "object",
11953 #[cfg(feature = "python")]
11954 JValue::LazyPyDict(_) => "object",
11955 _ => "unknown",
11956 }
11957 }
11958
11959 /// Ordered comparison with null/type checking shared across <, <=, >, >=
11960 ///
11961 /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
11962 /// `compare_strs` receives (left_str, right_str) for string operands.
11963 /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
11964 fn ordered_compare(
11965 &self,
11966 left: &JValue,
11967 right: &JValue,
11968 left_is_explicit_null: bool,
11969 right_is_explicit_null: bool,
11970 op_symbol: &str,
11971 compare_nums: fn(f64, f64) -> bool,
11972 compare_strs: fn(&str, &str) -> bool,
11973 ) -> Result<JValue, EvaluatorError> {
11974 match (left, right) {
11975 (JValue::Number(a), JValue::Number(b)) => {
11976 Ok(JValue::Bool(compare_nums(*a, *b)))
11977 }
11978 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
11979 // Both null/undefined -> return undefined
11980 (JValue::Null, JValue::Null) => Ok(JValue::Null),
11981 // Explicit null literal with any type (except null) -> T2010 error
11982 (JValue::Null, _) if left_is_explicit_null => {
11983 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11984 }
11985 (_, JValue::Null) if right_is_explicit_null => {
11986 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11987 }
11988 // Boolean with undefined -> T2010 error
11989 (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
11990 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
11991 }
11992 // Number or String with undefined (not explicit null) -> undefined result
11993 (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
11994 (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
11995 Ok(JValue::Null)
11996 }
11997 // String vs Number -> T2009
11998 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
11999 Err(EvaluatorError::EvaluationError(format!(
12000 "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
12001 op_symbol
12002 )))
12003 }
12004 // Boolean comparisons -> T2010
12005 (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
12006 Err(EvaluatorError::EvaluationError(format!(
12007 "T2010: Cannot compare {} and {}",
12008 Self::type_name(left), Self::type_name(right)
12009 )))
12010 }
12011 // Other type mismatches
12012 _ => Err(EvaluatorError::EvaluationError(format!(
12013 "T2010: Cannot compare {} and {}",
12014 Self::type_name(left), Self::type_name(right)
12015 ))),
12016 }
12017 }
12018
12019 /// Less than comparison
12020 fn less_than(
12021 &self,
12022 left: &JValue,
12023 right: &JValue,
12024 left_is_explicit_null: bool,
12025 right_is_explicit_null: bool,
12026 ) -> Result<JValue, EvaluatorError> {
12027 self.ordered_compare(
12028 left,
12029 right,
12030 left_is_explicit_null,
12031 right_is_explicit_null,
12032 "<",
12033 |a, b| a < b,
12034 |a, b| a < b,
12035 )
12036 }
12037
12038 /// Less than or equal comparison
12039 fn less_than_or_equal(
12040 &self,
12041 left: &JValue,
12042 right: &JValue,
12043 left_is_explicit_null: bool,
12044 right_is_explicit_null: bool,
12045 ) -> Result<JValue, EvaluatorError> {
12046 self.ordered_compare(
12047 left,
12048 right,
12049 left_is_explicit_null,
12050 right_is_explicit_null,
12051 "<=",
12052 |a, b| a <= b,
12053 |a, b| a <= b,
12054 )
12055 }
12056
12057 /// Greater than comparison
12058 fn greater_than(
12059 &self,
12060 left: &JValue,
12061 right: &JValue,
12062 left_is_explicit_null: bool,
12063 right_is_explicit_null: bool,
12064 ) -> Result<JValue, EvaluatorError> {
12065 self.ordered_compare(
12066 left,
12067 right,
12068 left_is_explicit_null,
12069 right_is_explicit_null,
12070 ">",
12071 |a, b| a > b,
12072 |a, b| a > b,
12073 )
12074 }
12075
12076 /// Greater than or equal comparison
12077 fn greater_than_or_equal(
12078 &self,
12079 left: &JValue,
12080 right: &JValue,
12081 left_is_explicit_null: bool,
12082 right_is_explicit_null: bool,
12083 ) -> Result<JValue, EvaluatorError> {
12084 self.ordered_compare(
12085 left,
12086 right,
12087 left_is_explicit_null,
12088 right_is_explicit_null,
12089 ">=",
12090 |a, b| a >= b,
12091 |a, b| a >= b,
12092 )
12093 }
12094
12095 /// Convert a value to a string for concatenation
12096 fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
12097 // Normalize a lazy operand up front: `functions::string::string`'s lazy arm maps a
12098 // conversion failure to `JValue::Null` (silently stringifying to `""`), which would
12099 // swallow the TypeError this must raise instead. Guarded by `is_lazy` so the
12100 // common (non-lazy) path pays no clone.
12101 let normalized;
12102 let value = if value.is_lazy() {
12103 normalized = normalize_lazy(value)?;
12104 &normalized
12105 } else {
12106 value
12107 };
12108 match value {
12109 JValue::String(s) => Ok(s.to_string()),
12110 JValue::Null => Ok(String::new()),
12111 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
12112 match crate::functions::string::string(value, None) {
12113 Ok(JValue::String(s)) => Ok(s.to_string()),
12114 Ok(JValue::Null) => Ok(String::new()),
12115 _ => Err(EvaluatorError::TypeError(
12116 "Cannot concatenate complex types".to_string(),
12117 )),
12118 }
12119 }
12120 _ => Ok(String::new()),
12121 }
12122 }
12123
12124 /// String concatenation
12125 fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12126 let left_str = Self::value_to_concat_string(left)?;
12127 let right_str = Self::value_to_concat_string(right)?;
12128 Ok(JValue::string(format!("{}{}", left_str, right_str)))
12129 }
12130
12131 /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
12132 fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12133 // Check left operand is a number or null
12134 let start_f64 = match left {
12135 JValue::Number(n) => Some(*n),
12136 JValue::Null | JValue::Undefined => None,
12137 _ => {
12138 return Err(EvaluatorError::EvaluationError(
12139 "T2003: Left operand of range operator must be a number".to_string(),
12140 ));
12141 }
12142 };
12143
12144 // Check left operand is an integer (if it's a number)
12145 if let Some(val) = start_f64 {
12146 if val.fract() != 0.0 {
12147 return Err(EvaluatorError::EvaluationError(
12148 "T2003: Left operand of range operator must be an integer".to_string(),
12149 ));
12150 }
12151 }
12152
12153 // Check right operand is a number or null
12154 let end_f64 = match right {
12155 JValue::Number(n) => Some(*n),
12156 JValue::Null | JValue::Undefined => None,
12157 _ => {
12158 return Err(EvaluatorError::EvaluationError(
12159 "T2004: Right operand of range operator must be a number".to_string(),
12160 ));
12161 }
12162 };
12163
12164 // Check right operand is an integer (if it's a number)
12165 if let Some(val) = end_f64 {
12166 if val.fract() != 0.0 {
12167 return Err(EvaluatorError::EvaluationError(
12168 "T2004: Right operand of range operator must be an integer".to_string(),
12169 ));
12170 }
12171 }
12172
12173 // If either operand is null, return empty array
12174 if start_f64.is_none() || end_f64.is_none() {
12175 return Ok(JValue::array(vec![]));
12176 }
12177
12178 let start = start_f64.unwrap() as i64;
12179 let end = end_f64.unwrap() as i64;
12180
12181 // Check range size limit (10 million elements max)
12182 let size = if start <= end {
12183 (end - start + 1) as usize
12184 } else {
12185 0
12186 };
12187 if size > 10_000_000 {
12188 return Err(EvaluatorError::EvaluationError(
12189 "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
12190 ));
12191 }
12192 check_sequence_length(size, &self.options)?;
12193
12194 let mut result = Vec::with_capacity(size);
12195 if start <= end {
12196 for i in start..=end {
12197 result.push(JValue::Number(i as f64));
12198 }
12199 }
12200 // Note: if start > end, return empty array (not reversed)
12201 Ok(JValue::array(result))
12202 }
12203
12204 /// In operator (checks if left is in right array/object)
12205 /// Array indexing: array[index]
12206 fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
12207 match (array, index) {
12208 (JValue::Array(arr), JValue::Number(n)) => {
12209 let idx = *n as i64;
12210 let len = arr.len() as i64;
12211
12212 // Handle negative indexing (offset from end)
12213 let actual_idx = if idx < 0 { len + idx } else { idx };
12214
12215 if actual_idx < 0 || actual_idx >= len {
12216 Ok(JValue::Undefined)
12217 } else {
12218 Ok(arr[actual_idx as usize].clone())
12219 }
12220 }
12221 _ => Err(EvaluatorError::TypeError(
12222 "Array indexing requires array and number".to_string(),
12223 )),
12224 }
12225 }
12226
12227 /// Array filtering: array[predicate]
12228 /// Evaluates the predicate for each item in the array and returns items where predicate is true
12229 fn array_filter(
12230 &mut self,
12231 _lhs_node: &AstNode,
12232 rhs_node: &AstNode,
12233 array: &JValue,
12234 _original_data: &JValue,
12235 ) -> Result<JValue, EvaluatorError> {
12236 match array {
12237 JValue::Array(arr) => {
12238 // Pre-allocate with estimated capacity (assume ~50% will match)
12239 let mut filtered = Vec::with_capacity(arr.len() / 2);
12240
12241 for item in arr.iter() {
12242 // Evaluate the predicate in the context of this array item
12243 // The item becomes the new "current context" ($)
12244 let predicate_result = self.evaluate_internal(rhs_node, item)?;
12245
12246 // Check if the predicate is truthy
12247 if self.is_truthy(&predicate_result) {
12248 filtered.push(item.clone());
12249 }
12250 }
12251
12252 Ok(JValue::array(filtered))
12253 }
12254 _ => Err(EvaluatorError::TypeError(
12255 "Array filtering requires an array".to_string(),
12256 )),
12257 }
12258 }
12259
12260 fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
12261 // If either side is undefined/null, return false (not an error)
12262 // This matches JavaScript behavior
12263 if left.is_null() || right.is_null() {
12264 return Ok(JValue::Bool(false));
12265 }
12266
12267 // Normalize a lazy left operand once; every equality below needs a real
12268 // value, not a per-comparison materialization attempt. Guarded by `is_lazy`
12269 // so the common (non-lazy) path pays no clone.
12270 let normalized_left;
12271 let left = if left.is_lazy() {
12272 normalized_left = normalize_lazy(left)?;
12273 &normalized_left
12274 } else {
12275 left
12276 };
12277
12278 match right {
12279 JValue::Array(arr) => {
12280 for v in arr.iter() {
12281 // compiled_equal normalizes a lazy element (guarded) so a
12282 // conversion failure raises instead of silently not matching.
12283 if matches!(compiled_equal(left, v)?, JValue::Bool(true)) {
12284 return Ok(JValue::Bool(true));
12285 }
12286 }
12287 Ok(JValue::Bool(false))
12288 }
12289 JValue::Object(obj) => {
12290 if let JValue::String(key) = left {
12291 Ok(JValue::Bool(obj.contains_key(&**key)))
12292 } else {
12293 Ok(JValue::Bool(false))
12294 }
12295 }
12296 // Right-side lazy-object key-presence check doesn't convert any values
12297 // (`contains_field` checks the Python dict directly), so it's left as-is.
12298 #[cfg(feature = "python")]
12299 JValue::LazyPyDict(lazy) => {
12300 if let JValue::String(key) = left {
12301 Ok(JValue::Bool(lazy.contains_field(key)))
12302 } else {
12303 Ok(JValue::Bool(false))
12304 }
12305 }
12306 // If right side is not an array or object (e.g., string, number),
12307 // wrap it in an array for comparison
12308 other => Ok(JValue::Bool(self.equals(left, other))),
12309 }
12310 }
12311
12312 /// Create a partially applied function from a function call with placeholder arguments
12313 /// This evaluates non-placeholder arguments and creates a new lambda that takes
12314 /// the placeholder positions as parameters.
12315 fn create_partial_application(
12316 &mut self,
12317 name: &str,
12318 args: &[AstNode],
12319 is_builtin: bool,
12320 data: &JValue,
12321 ) -> Result<JValue, EvaluatorError> {
12322 // First, look up the function to ensure it exists
12323 let is_lambda = self.context.lookup_lambda(name).is_some()
12324 || (self
12325 .context
12326 .lookup(name)
12327 .map(|v| matches!(v, JValue::Lambda { .. }))
12328 .unwrap_or(false));
12329
12330 // Built-in functions must be called with $ prefix for partial application
12331 // Without $, it's an error (T1007) suggesting the user forgot the $
12332 if !is_lambda && !is_builtin {
12333 // Check if it's a built-in function called without $
12334 if self.is_builtin_function(name) {
12335 return Err(EvaluatorError::EvaluationError(format!(
12336 "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
12337 name
12338 )));
12339 }
12340 return Err(EvaluatorError::EvaluationError(
12341 "T1008: Attempted to partially apply a non-function".to_string(),
12342 ));
12343 }
12344
12345 // Evaluate non-placeholder arguments and track placeholder positions
12346 let mut bound_args: Vec<(usize, JValue)> = Vec::new();
12347 let mut placeholder_positions: Vec<usize> = Vec::new();
12348
12349 for (i, arg) in args.iter().enumerate() {
12350 if matches!(arg, AstNode::Placeholder) {
12351 placeholder_positions.push(i);
12352 } else {
12353 let value = self.evaluate_internal(arg, data)?;
12354 bound_args.push((i, value));
12355 }
12356 }
12357
12358 // Generate parameter names for each placeholder
12359 let param_names: Vec<String> = placeholder_positions
12360 .iter()
12361 .enumerate()
12362 .map(|(i, _)| format!("__p{}", i))
12363 .collect();
12364
12365 // Store the partial application info as a special lambda
12366 // When invoked, it will call the original function with bound + placeholder args
12367 let partial_id = format!(
12368 "__partial_{}_{}_{}",
12369 name,
12370 placeholder_positions.len(),
12371 bound_args.len()
12372 );
12373
12374 // Create a stored lambda that represents this partial application
12375 // The body is a marker that we'll interpret specially during invocation
12376 let stored_lambda = StoredLambda {
12377 params: param_names.clone(),
12378 body: AstNode::String(format!(
12379 "__partial_call:{}:{}:{}",
12380 name,
12381 is_builtin,
12382 args.len()
12383 )),
12384 compiled_body: None, // Partial application uses a special body marker
12385 signature: None,
12386 captured_env: {
12387 let mut env = self.capture_current_environment();
12388 // Store the bound arguments in the captured environment
12389 for (pos, value) in &bound_args {
12390 env.insert(format!("__bound_arg_{}", pos), value.clone());
12391 }
12392 // Store placeholder positions
12393 env.insert(
12394 "__placeholder_positions".to_string(),
12395 JValue::array(
12396 placeholder_positions
12397 .iter()
12398 .map(|p| JValue::Number(*p as f64))
12399 .collect::<Vec<_>>(),
12400 ),
12401 );
12402 // Store total argument count
12403 env.insert(
12404 "__total_args".to_string(),
12405 JValue::Number(args.len() as f64),
12406 );
12407 env
12408 },
12409 captured_data: Some(data.clone()),
12410 thunk: false,
12411 };
12412
12413 self.context.bind_lambda(partial_id.clone(), stored_lambda);
12414
12415 // Return a lambda object that can be invoked
12416 let lambda_obj = JValue::lambda(
12417 partial_id.as_str(),
12418 param_names,
12419 Some(name.to_string()),
12420 None::<String>,
12421 );
12422
12423 Ok(lambda_obj)
12424 }
12425}
12426
12427impl Default for Evaluator {
12428 fn default() -> Self {
12429 Self::new()
12430 }
12431}
12432
12433#[cfg(test)]
12434mod tests {
12435 use super::*;
12436 use crate::ast::{BinaryOp, UnaryOp};
12437
12438 // --- Task 7: tuple-wrapper output leak -----------------------------------
12439 //
12440 // `%`/`@`/`#` are implemented internally via a tuple-stream representation
12441 // (`create_tuple_stream`): each element gets wrapped as
12442 // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
12443 // consume/re-wrap these, but the *final* evaluate() result can still carry
12444 // a lingering wrapper -- confirmed for real by dumping actual output before
12445 // this fix (see task-7-report.md for the raw before/after). These tests
12446 // pin both the bare top-level case (Task 5's brief `#` example) and the
12447 // object/array-construction-nested case (found while verifying the brief's
12448 // illustrative fix against real output -- a plain per-element Array-only
12449 // recursion does not reach into a constructed object's field values).
12450
12451 fn dataset5_for_tuple_tests() -> JValue {
12452 let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
12453 serde_json::from_str::<serde_json::Value>(s).unwrap().into()
12454 }
12455
12456 fn assert_no_tuple_wrapper(value: &JValue) {
12457 match value {
12458 JValue::Object(obj) => {
12459 assert!(
12460 obj.get("__tuple__").is_none(),
12461 "tuple wrapper leaked into output: {:?}",
12462 value
12463 );
12464 for v in obj.values() {
12465 assert_no_tuple_wrapper(v);
12466 }
12467 }
12468 JValue::Array(arr) => {
12469 for item in arr.iter() {
12470 assert_no_tuple_wrapper(item);
12471 }
12472 }
12473 _ => {}
12474 }
12475 }
12476
12477 #[test]
12478 fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
12479 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12480 let ast = crate::parser::parse("items#$i").unwrap();
12481 let mut evaluator = Evaluator::new();
12482 let result = evaluator.evaluate(&ast, &data).unwrap();
12483 assert_no_tuple_wrapper(&result);
12484 assert_eq!(
12485 result,
12486 JValue::array(vec![
12487 JValue::from(1i64),
12488 JValue::from(2i64),
12489 JValue::from(3i64)
12490 ])
12491 );
12492 }
12493
12494 #[test]
12495 fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
12496 // Confirmed by Task 6 to evaluate to the correct @-values but stay
12497 // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
12498 let data = dataset5_for_tuple_tests();
12499 let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
12500 let mut evaluator = Evaluator::new();
12501 let result = evaluator.evaluate(&ast, &data).unwrap();
12502 assert_no_tuple_wrapper(&result);
12503 assert_eq!(
12504 result,
12505 JValue::array(vec![
12506 JValue::string("040657863"),
12507 JValue::string("0406654603"),
12508 ])
12509 );
12510 }
12511
12512 #[test]
12513 fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
12514 // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
12515 let data = dataset5_for_tuple_tests();
12516 let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
12517 .unwrap();
12518 let mut evaluator = Evaluator::new();
12519 let result = evaluator.evaluate(&ast, &data).unwrap();
12520 assert_no_tuple_wrapper(&result);
12521 assert_eq!(
12522 result,
12523 JValue::array(vec![
12524 JValue::string("0406654608"),
12525 JValue::string("0406634348"),
12526 ])
12527 );
12528 }
12529
12530 #[test]
12531 fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
12532 // A tuple-producing expression nested inside a constructed object's field
12533 // value: the top-level result is a plain (non-tuple) Object, so a naive
12534 // "unwrap only if the whole value is a tuple wrapper" check would miss
12535 // this -- must recurse into field values too.
12536 let data = dataset5_for_tuple_tests();
12537 let ast =
12538 crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
12539 .unwrap();
12540 let mut evaluator = Evaluator::new();
12541 let result = evaluator.evaluate(&ast, &data).unwrap();
12542 assert_no_tuple_wrapper(&result);
12543 assert_eq!(
12544 result,
12545 JValue::from(serde_json::json!({
12546 "skus": ["040657863", "0406654603"]
12547 }))
12548 );
12549 }
12550
12551 #[test]
12552 fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
12553 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
12554 let ast = crate::parser::parse("[items#$i]").unwrap();
12555 let mut evaluator = Evaluator::new();
12556 let result = evaluator.evaluate(&ast, &data).unwrap();
12557 assert_no_tuple_wrapper(&result);
12558 }
12559
12560 #[test]
12561 fn test_evaluate_literals() {
12562 let mut evaluator = Evaluator::new();
12563 let data = JValue::Null;
12564
12565 // String literal
12566 let result = evaluator
12567 .evaluate(&AstNode::string("hello"), &data)
12568 .unwrap();
12569 assert_eq!(result, JValue::string("hello"));
12570
12571 // Number literal
12572 let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
12573 assert_eq!(result, JValue::from(42i64));
12574
12575 // Boolean literal
12576 let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
12577 assert_eq!(result, JValue::Bool(true));
12578
12579 // Null literal
12580 let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
12581 assert_eq!(result, JValue::Null);
12582 }
12583
12584 #[test]
12585 fn test_evaluate_variables() {
12586 let mut evaluator = Evaluator::new();
12587 let data = JValue::Null;
12588
12589 // Bind a variable
12590 evaluator
12591 .context
12592 .bind("x".to_string(), JValue::from(100i64));
12593
12594 // Look up the variable
12595 let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
12596 assert_eq!(result, JValue::from(100i64));
12597
12598 // Undefined variable returns null (undefined in JSONata semantics)
12599 let result = evaluator
12600 .evaluate(&AstNode::variable("undefined"), &data)
12601 .unwrap();
12602 assert_eq!(result, JValue::Null);
12603 }
12604
12605 #[test]
12606 fn test_evaluate_path() {
12607 let mut evaluator = Evaluator::new();
12608 let data = JValue::from(serde_json::json!({
12609 "foo": {
12610 "bar": {
12611 "baz": 42
12612 }
12613 }
12614 }));
12615 // Simple path
12616 let path = AstNode::Path {
12617 steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
12618 };
12619 let result = evaluator.evaluate(&path, &data).unwrap();
12620 assert_eq!(
12621 result,
12622 JValue::from(serde_json::json!({"bar": {"baz": 42}}))
12623 );
12624
12625 // Nested path
12626 let path = AstNode::Path {
12627 steps: vec![
12628 PathStep::new(AstNode::Name("foo".to_string())),
12629 PathStep::new(AstNode::Name("bar".to_string())),
12630 PathStep::new(AstNode::Name("baz".to_string())),
12631 ],
12632 };
12633 let result = evaluator.evaluate(&path, &data).unwrap();
12634 assert_eq!(result, JValue::from(42i64));
12635
12636 // Missing path returns undefined (not null - see issue #32)
12637 let path = AstNode::Path {
12638 steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
12639 };
12640 let result = evaluator.evaluate(&path, &data).unwrap();
12641 assert_eq!(result, JValue::Undefined);
12642 }
12643
12644 #[test]
12645 fn test_arithmetic_operations() {
12646 let mut evaluator = Evaluator::new();
12647 let data = JValue::Null;
12648
12649 // Addition
12650 let expr = AstNode::Binary {
12651 op: BinaryOp::Add,
12652 lhs: Box::new(AstNode::number(10.0)),
12653 rhs: Box::new(AstNode::number(5.0)),
12654 };
12655 let result = evaluator.evaluate(&expr, &data).unwrap();
12656 assert_eq!(result, JValue::Number(15.0));
12657
12658 // Subtraction
12659 let expr = AstNode::Binary {
12660 op: BinaryOp::Subtract,
12661 lhs: Box::new(AstNode::number(10.0)),
12662 rhs: Box::new(AstNode::number(5.0)),
12663 };
12664 let result = evaluator.evaluate(&expr, &data).unwrap();
12665 assert_eq!(result, JValue::Number(5.0));
12666
12667 // Multiplication
12668 let expr = AstNode::Binary {
12669 op: BinaryOp::Multiply,
12670 lhs: Box::new(AstNode::number(10.0)),
12671 rhs: Box::new(AstNode::number(5.0)),
12672 };
12673 let result = evaluator.evaluate(&expr, &data).unwrap();
12674 assert_eq!(result, JValue::Number(50.0));
12675
12676 // Division
12677 let expr = AstNode::Binary {
12678 op: BinaryOp::Divide,
12679 lhs: Box::new(AstNode::number(10.0)),
12680 rhs: Box::new(AstNode::number(5.0)),
12681 };
12682 let result = evaluator.evaluate(&expr, &data).unwrap();
12683 assert_eq!(result, JValue::Number(2.0));
12684
12685 // Modulo
12686 let expr = AstNode::Binary {
12687 op: BinaryOp::Modulo,
12688 lhs: Box::new(AstNode::number(10.0)),
12689 rhs: Box::new(AstNode::number(3.0)),
12690 };
12691 let result = evaluator.evaluate(&expr, &data).unwrap();
12692 assert_eq!(result, JValue::Number(1.0));
12693 }
12694
12695 #[test]
12696 fn test_division_by_zero() {
12697 let mut evaluator = Evaluator::new();
12698 let data = JValue::Null;
12699
12700 let expr = AstNode::Binary {
12701 op: BinaryOp::Divide,
12702 lhs: Box::new(AstNode::number(10.0)),
12703 rhs: Box::new(AstNode::number(0.0)),
12704 };
12705 let result = evaluator.evaluate(&expr, &data);
12706 assert!(result.is_err());
12707 }
12708
12709 #[test]
12710 fn test_comparison_operations() {
12711 let mut evaluator = Evaluator::new();
12712 let data = JValue::Null;
12713
12714 // Equal
12715 let expr = AstNode::Binary {
12716 op: BinaryOp::Equal,
12717 lhs: Box::new(AstNode::number(5.0)),
12718 rhs: Box::new(AstNode::number(5.0)),
12719 };
12720 assert_eq!(
12721 evaluator.evaluate(&expr, &data).unwrap(),
12722 JValue::Bool(true)
12723 );
12724
12725 // Not equal
12726 let expr = AstNode::Binary {
12727 op: BinaryOp::NotEqual,
12728 lhs: Box::new(AstNode::number(5.0)),
12729 rhs: Box::new(AstNode::number(3.0)),
12730 };
12731 assert_eq!(
12732 evaluator.evaluate(&expr, &data).unwrap(),
12733 JValue::Bool(true)
12734 );
12735
12736 // Less than
12737 let expr = AstNode::Binary {
12738 op: BinaryOp::LessThan,
12739 lhs: Box::new(AstNode::number(3.0)),
12740 rhs: Box::new(AstNode::number(5.0)),
12741 };
12742 assert_eq!(
12743 evaluator.evaluate(&expr, &data).unwrap(),
12744 JValue::Bool(true)
12745 );
12746
12747 // Greater than
12748 let expr = AstNode::Binary {
12749 op: BinaryOp::GreaterThan,
12750 lhs: Box::new(AstNode::number(5.0)),
12751 rhs: Box::new(AstNode::number(3.0)),
12752 };
12753 assert_eq!(
12754 evaluator.evaluate(&expr, &data).unwrap(),
12755 JValue::Bool(true)
12756 );
12757 }
12758
12759 #[test]
12760 fn test_logical_operations() {
12761 let mut evaluator = Evaluator::new();
12762 let data = JValue::Null;
12763
12764 // And - both true
12765 let expr = AstNode::Binary {
12766 op: BinaryOp::And,
12767 lhs: Box::new(AstNode::boolean(true)),
12768 rhs: Box::new(AstNode::boolean(true)),
12769 };
12770 assert_eq!(
12771 evaluator.evaluate(&expr, &data).unwrap(),
12772 JValue::Bool(true)
12773 );
12774
12775 // And - first false
12776 let expr = AstNode::Binary {
12777 op: BinaryOp::And,
12778 lhs: Box::new(AstNode::boolean(false)),
12779 rhs: Box::new(AstNode::boolean(true)),
12780 };
12781 assert_eq!(
12782 evaluator.evaluate(&expr, &data).unwrap(),
12783 JValue::Bool(false)
12784 );
12785
12786 // Or - first true
12787 let expr = AstNode::Binary {
12788 op: BinaryOp::Or,
12789 lhs: Box::new(AstNode::boolean(true)),
12790 rhs: Box::new(AstNode::boolean(false)),
12791 };
12792 assert_eq!(
12793 evaluator.evaluate(&expr, &data).unwrap(),
12794 JValue::Bool(true)
12795 );
12796
12797 // Or - both false
12798 let expr = AstNode::Binary {
12799 op: BinaryOp::Or,
12800 lhs: Box::new(AstNode::boolean(false)),
12801 rhs: Box::new(AstNode::boolean(false)),
12802 };
12803 assert_eq!(
12804 evaluator.evaluate(&expr, &data).unwrap(),
12805 JValue::Bool(false)
12806 );
12807 }
12808
12809 #[test]
12810 fn test_string_concatenation() {
12811 let mut evaluator = Evaluator::new();
12812 let data = JValue::Null;
12813
12814 let expr = AstNode::Binary {
12815 op: BinaryOp::Concatenate,
12816 lhs: Box::new(AstNode::string("Hello")),
12817 rhs: Box::new(AstNode::string(" World")),
12818 };
12819 let result = evaluator.evaluate(&expr, &data).unwrap();
12820 assert_eq!(result, JValue::string("Hello World"));
12821 }
12822
12823 #[test]
12824 fn test_range_operator() {
12825 let mut evaluator = Evaluator::new();
12826 let data = JValue::Null;
12827
12828 // Forward range
12829 let expr = AstNode::Binary {
12830 op: BinaryOp::Range,
12831 lhs: Box::new(AstNode::number(1.0)),
12832 rhs: Box::new(AstNode::number(5.0)),
12833 };
12834 let result = evaluator.evaluate(&expr, &data).unwrap();
12835 assert_eq!(
12836 result,
12837 JValue::array(vec![
12838 JValue::Number(1.0),
12839 JValue::Number(2.0),
12840 JValue::Number(3.0),
12841 JValue::Number(4.0),
12842 JValue::Number(5.0)
12843 ])
12844 );
12845
12846 // Backward range (start > end) returns empty array
12847 let expr = AstNode::Binary {
12848 op: BinaryOp::Range,
12849 lhs: Box::new(AstNode::number(5.0)),
12850 rhs: Box::new(AstNode::number(1.0)),
12851 };
12852 let result = evaluator.evaluate(&expr, &data).unwrap();
12853 assert_eq!(result, JValue::array(vec![]));
12854 }
12855
12856 #[test]
12857 fn test_in_operator() {
12858 let mut evaluator = Evaluator::new();
12859 let data = JValue::Null;
12860
12861 // In array
12862 let expr = AstNode::Binary {
12863 op: BinaryOp::In,
12864 lhs: Box::new(AstNode::number(3.0)),
12865 rhs: Box::new(AstNode::Array(vec![
12866 AstNode::number(1.0),
12867 AstNode::number(2.0),
12868 AstNode::number(3.0),
12869 ])),
12870 };
12871 let result = evaluator.evaluate(&expr, &data).unwrap();
12872 assert_eq!(result, JValue::Bool(true));
12873
12874 // Not in array
12875 let expr = AstNode::Binary {
12876 op: BinaryOp::In,
12877 lhs: Box::new(AstNode::number(5.0)),
12878 rhs: Box::new(AstNode::Array(vec![
12879 AstNode::number(1.0),
12880 AstNode::number(2.0),
12881 AstNode::number(3.0),
12882 ])),
12883 };
12884 let result = evaluator.evaluate(&expr, &data).unwrap();
12885 assert_eq!(result, JValue::Bool(false));
12886 }
12887
12888 #[test]
12889 fn test_unary_operations() {
12890 let mut evaluator = Evaluator::new();
12891 let data = JValue::Null;
12892
12893 // Negation
12894 let expr = AstNode::Unary {
12895 op: UnaryOp::Negate,
12896 operand: Box::new(AstNode::number(5.0)),
12897 };
12898 let result = evaluator.evaluate(&expr, &data).unwrap();
12899 assert_eq!(result, JValue::Number(-5.0));
12900
12901 // Not
12902 let expr = AstNode::Unary {
12903 op: UnaryOp::Not,
12904 operand: Box::new(AstNode::boolean(true)),
12905 };
12906 let result = evaluator.evaluate(&expr, &data).unwrap();
12907 assert_eq!(result, JValue::Bool(false));
12908 }
12909
12910 #[test]
12911 fn test_array_construction() {
12912 let mut evaluator = Evaluator::new();
12913 let data = JValue::Null;
12914
12915 let expr = AstNode::Array(vec![
12916 AstNode::number(1.0),
12917 AstNode::number(2.0),
12918 AstNode::number(3.0),
12919 ]);
12920 let result = evaluator.evaluate(&expr, &data).unwrap();
12921 // Whole number literals are preserved as integers
12922 assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
12923 }
12924
12925 #[test]
12926 fn test_object_construction() {
12927 let mut evaluator = Evaluator::new();
12928 let data = JValue::Null;
12929
12930 let expr = AstNode::Object(vec![
12931 (AstNode::string("name"), AstNode::string("Alice")),
12932 (AstNode::string("age"), AstNode::number(30.0)),
12933 ]);
12934 let result = evaluator.evaluate(&expr, &data).unwrap();
12935 // Whole number literals are preserved as integers
12936 let mut expected = IndexMap::new();
12937 expected.insert("name".to_string(), JValue::string("Alice"));
12938 expected.insert("age".to_string(), JValue::Number(30.0));
12939 assert_eq!(result, JValue::object(expected));
12940 }
12941
12942 #[test]
12943 fn test_conditional() {
12944 let mut evaluator = Evaluator::new();
12945 let data = JValue::Null;
12946
12947 // True condition
12948 let expr = AstNode::Conditional {
12949 condition: Box::new(AstNode::boolean(true)),
12950 then_branch: Box::new(AstNode::string("yes")),
12951 else_branch: Some(Box::new(AstNode::string("no"))),
12952 };
12953 let result = evaluator.evaluate(&expr, &data).unwrap();
12954 assert_eq!(result, JValue::string("yes"));
12955
12956 // False condition
12957 let expr = AstNode::Conditional {
12958 condition: Box::new(AstNode::boolean(false)),
12959 then_branch: Box::new(AstNode::string("yes")),
12960 else_branch: Some(Box::new(AstNode::string("no"))),
12961 };
12962 let result = evaluator.evaluate(&expr, &data).unwrap();
12963 assert_eq!(result, JValue::string("no"));
12964
12965 // No else branch returns undefined (not null)
12966 let expr = AstNode::Conditional {
12967 condition: Box::new(AstNode::boolean(false)),
12968 then_branch: Box::new(AstNode::string("yes")),
12969 else_branch: None,
12970 };
12971 let result = evaluator.evaluate(&expr, &data).unwrap();
12972 assert_eq!(result, JValue::Undefined);
12973 }
12974
12975 #[test]
12976 fn test_block_expression() {
12977 let mut evaluator = Evaluator::new();
12978 let data = JValue::Null;
12979
12980 let expr = AstNode::Block(vec![
12981 AstNode::number(1.0),
12982 AstNode::number(2.0),
12983 AstNode::number(3.0),
12984 ]);
12985 let result = evaluator.evaluate(&expr, &data).unwrap();
12986 // Block returns the last expression; whole numbers are preserved as integers
12987 assert_eq!(result, JValue::from(3i64));
12988 }
12989
12990 #[test]
12991 fn test_function_calls() {
12992 let mut evaluator = Evaluator::new();
12993 let data = JValue::Null;
12994
12995 // uppercase function
12996 let expr = AstNode::Function {
12997 name: "uppercase".to_string(),
12998 args: vec![AstNode::string("hello")],
12999 is_builtin: true,
13000 };
13001 let result = evaluator.evaluate(&expr, &data).unwrap();
13002 assert_eq!(result, JValue::string("HELLO"));
13003
13004 // lowercase function
13005 let expr = AstNode::Function {
13006 name: "lowercase".to_string(),
13007 args: vec![AstNode::string("HELLO")],
13008 is_builtin: true,
13009 };
13010 let result = evaluator.evaluate(&expr, &data).unwrap();
13011 assert_eq!(result, JValue::string("hello"));
13012
13013 // length function
13014 let expr = AstNode::Function {
13015 name: "length".to_string(),
13016 args: vec![AstNode::string("hello")],
13017 is_builtin: true,
13018 };
13019 let result = evaluator.evaluate(&expr, &data).unwrap();
13020 assert_eq!(result, JValue::from(5i64));
13021
13022 // sum function
13023 let expr = AstNode::Function {
13024 name: "sum".to_string(),
13025 args: vec![AstNode::Array(vec![
13026 AstNode::number(1.0),
13027 AstNode::number(2.0),
13028 AstNode::number(3.0),
13029 ])],
13030 is_builtin: true,
13031 };
13032 let result = evaluator.evaluate(&expr, &data).unwrap();
13033 assert_eq!(result, JValue::Number(6.0));
13034
13035 // count function
13036 let expr = AstNode::Function {
13037 name: "count".to_string(),
13038 args: vec![AstNode::Array(vec![
13039 AstNode::number(1.0),
13040 AstNode::number(2.0),
13041 AstNode::number(3.0),
13042 ])],
13043 is_builtin: true,
13044 };
13045 let result = evaluator.evaluate(&expr, &data).unwrap();
13046 assert_eq!(result, JValue::from(3i64));
13047 }
13048
13049 #[test]
13050 fn test_complex_nested_data() {
13051 let mut evaluator = Evaluator::new();
13052 let data = JValue::from(serde_json::json!({
13053 "users": [
13054 {"name": "Alice", "age": 30},
13055 {"name": "Bob", "age": 25},
13056 {"name": "Charlie", "age": 35}
13057 ],
13058 "metadata": {
13059 "total": 3,
13060 "version": "1.0"
13061 }
13062 }));
13063 // Access nested field
13064 let path = AstNode::Path {
13065 steps: vec![
13066 PathStep::new(AstNode::Name("metadata".to_string())),
13067 PathStep::new(AstNode::Name("version".to_string())),
13068 ],
13069 };
13070 let result = evaluator.evaluate(&path, &data).unwrap();
13071 assert_eq!(result, JValue::string("1.0"));
13072 }
13073
13074 #[test]
13075 fn test_error_handling() {
13076 let mut evaluator = Evaluator::new();
13077 let data = JValue::Null;
13078
13079 // Type error: adding string and number
13080 let expr = AstNode::Binary {
13081 op: BinaryOp::Add,
13082 lhs: Box::new(AstNode::string("hello")),
13083 rhs: Box::new(AstNode::number(5.0)),
13084 };
13085 let result = evaluator.evaluate(&expr, &data);
13086 assert!(result.is_err());
13087
13088 // Reference error: undefined function
13089 let expr = AstNode::Function {
13090 name: "undefined_function".to_string(),
13091 args: vec![],
13092 is_builtin: false,
13093 };
13094 let result = evaluator.evaluate(&expr, &data);
13095 assert!(result.is_err());
13096 }
13097
13098 #[test]
13099 fn test_truthiness() {
13100 let evaluator = Evaluator::new();
13101
13102 assert!(!evaluator.is_truthy(&JValue::Null));
13103 assert!(!evaluator.is_truthy(&JValue::Bool(false)));
13104 assert!(evaluator.is_truthy(&JValue::Bool(true)));
13105 assert!(!evaluator.is_truthy(&JValue::from(0i64)));
13106 assert!(evaluator.is_truthy(&JValue::from(1i64)));
13107 assert!(!evaluator.is_truthy(&JValue::string("")));
13108 assert!(evaluator.is_truthy(&JValue::string("hello")));
13109 assert!(!evaluator.is_truthy(&JValue::array(vec![])));
13110 assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
13111 }
13112
13113 #[test]
13114 fn test_integration_with_parser() {
13115 use crate::parser::parse;
13116
13117 let mut evaluator = Evaluator::new();
13118 let data = JValue::from(serde_json::json!({
13119 "price": 10,
13120 "quantity": 5
13121 }));
13122 // Test simple path
13123 let ast = parse("price").unwrap();
13124 let result = evaluator.evaluate(&ast, &data).unwrap();
13125 assert_eq!(result, JValue::from(10i64));
13126
13127 // Test arithmetic
13128 let ast = parse("price * quantity").unwrap();
13129 let result = evaluator.evaluate(&ast, &data).unwrap();
13130 // Note: Arithmetic operations produce f64 results in JSON
13131 assert_eq!(result, JValue::Number(50.0));
13132
13133 // Test comparison
13134 let ast = parse("price > 5").unwrap();
13135 let result = evaluator.evaluate(&ast, &data).unwrap();
13136 assert_eq!(result, JValue::Bool(true));
13137 }
13138
13139 #[test]
13140 fn test_evaluate_dollar_function_uppercase() {
13141 use crate::parser::parse;
13142
13143 let mut evaluator = Evaluator::new();
13144 let ast = parse(r#"$uppercase("hello")"#).unwrap();
13145 let empty = JValue::object(IndexMap::new());
13146 let result = evaluator.evaluate(&ast, &empty).unwrap();
13147 assert_eq!(result, JValue::string("HELLO"));
13148 }
13149
13150 #[test]
13151 fn test_evaluate_dollar_function_sum() {
13152 use crate::parser::parse;
13153
13154 let mut evaluator = Evaluator::new();
13155 let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
13156 let empty = JValue::object(IndexMap::new());
13157 let result = evaluator.evaluate(&ast, &empty).unwrap();
13158 assert_eq!(result, JValue::Number(15.0));
13159 }
13160
13161 #[test]
13162 fn test_evaluate_nested_dollar_functions() {
13163 use crate::parser::parse;
13164
13165 let mut evaluator = Evaluator::new();
13166 let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
13167 let empty = JValue::object(IndexMap::new());
13168 let result = evaluator.evaluate(&ast, &empty).unwrap();
13169 // length() returns an integer, not a float
13170 assert_eq!(result, JValue::Number(5.0));
13171 }
13172
13173 #[test]
13174 fn test_array_mapping() {
13175 use crate::parser::parse;
13176
13177 let mut evaluator = Evaluator::new();
13178 let data: JValue = serde_json::from_str(
13179 r#"{
13180 "products": [
13181 {"id": 1, "name": "Laptop", "price": 999.99},
13182 {"id": 2, "name": "Mouse", "price": 29.99},
13183 {"id": 3, "name": "Keyboard", "price": 79.99}
13184 ]
13185 }"#,
13186 )
13187 .map(|v: serde_json::Value| JValue::from(v))
13188 .unwrap();
13189
13190 // Test mapping over array to extract field
13191 let ast = parse("products.name").unwrap();
13192 let result = evaluator.evaluate(&ast, &data).unwrap();
13193 assert_eq!(
13194 result,
13195 JValue::array(vec![
13196 JValue::string("Laptop"),
13197 JValue::string("Mouse"),
13198 JValue::string("Keyboard")
13199 ])
13200 );
13201
13202 // Test mapping over array to extract prices
13203 let ast = parse("products.price").unwrap();
13204 let result = evaluator.evaluate(&ast, &data).unwrap();
13205 assert_eq!(
13206 result,
13207 JValue::array(vec![
13208 JValue::Number(999.99),
13209 JValue::Number(29.99),
13210 JValue::Number(79.99)
13211 ])
13212 );
13213
13214 // Test with $sum function on mapped array
13215 let ast = parse("$sum(products.price)").unwrap();
13216 let result = evaluator.evaluate(&ast, &data).unwrap();
13217 assert_eq!(result, JValue::Number(1109.97));
13218 }
13219
13220 #[test]
13221 fn test_empty_brackets() {
13222 use crate::parser::parse;
13223
13224 let mut evaluator = Evaluator::new();
13225
13226 // Test empty brackets on simple value - should wrap in array
13227 let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
13228 let ast = parse("foo[]").unwrap();
13229 let result = evaluator.evaluate(&ast, &data).unwrap();
13230 assert_eq!(
13231 result,
13232 JValue::array(vec![JValue::string("bar")]),
13233 "Empty brackets should wrap value in array"
13234 );
13235
13236 // Test empty brackets on array - should return array as-is
13237 let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
13238 let ast2 = parse("arr[]").unwrap();
13239 let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
13240 assert_eq!(
13241 result2,
13242 JValue::array(vec![
13243 JValue::Number(1.0),
13244 JValue::Number(2.0),
13245 JValue::Number(3.0)
13246 ]),
13247 "Empty brackets should preserve array"
13248 );
13249 }
13250
13251 // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
13252 // Expected values below are ground-truthed against jsonata-js 2.x.
13253
13254 #[test]
13255 fn test_index_bind_makes_variable_available_in_next_step() {
13256 // `#$o` binds each Order's position; `$o` must resolve in the later step.
13257 let data: JValue = serde_json::json!({
13258 "Account": {
13259 "Order": [
13260 {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
13261 {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
13262 ]
13263 }
13264 })
13265 .into();
13266 let ast =
13267 crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
13268 let mut evaluator = Evaluator::new();
13269 let result = evaluator.evaluate(&ast, &data).unwrap();
13270 assert_eq!(
13271 result,
13272 serde_json::json!([
13273 {"name": "Hat", "idx": 0},
13274 {"name": "Cap", "idx": 1},
13275 {"name": "Sock", "idx": 1}
13276 ])
13277 .into()
13278 );
13279 }
13280
13281 #[test]
13282 fn test_index_bind_with_predicate_stage() {
13283 // Mirrors reference joins/index[13]: index binding, then a predicate on
13284 // the next step, carrying the index binding through.
13285 let data: JValue = serde_json::json!({
13286 "Account": {
13287 "Order": [
13288 {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
13289 {"Product": [{"ProductID": 9, "Name": "C"}]}
13290 ]
13291 }
13292 })
13293 .into();
13294 let ast =
13295 crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
13296 .unwrap();
13297 let mut evaluator = Evaluator::new();
13298 let result = evaluator.evaluate(&ast, &data).unwrap();
13299 assert_eq!(
13300 result,
13301 serde_json::json!([
13302 {"n": "B", "idx": 0},
13303 {"n": "C", "idx": 1}
13304 ])
13305 .into()
13306 );
13307 }
13308
13309 #[test]
13310 fn test_focus_bind_makes_variable_available_in_next_step() {
13311 // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
13312 // does NOT advance the context `@`); the variable itself is what carries
13313 // forward. This asserts the real jsonata-js behaviour.
13314 let data: JValue = serde_json::json!({
13315 "Account": {
13316 "Order": [
13317 {"OrderID": "o1"},
13318 {"OrderID": "o2"}
13319 ]
13320 }
13321 })
13322 .into();
13323 let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
13324 let mut evaluator = Evaluator::new();
13325 let result = evaluator.evaluate(&ast, &data).unwrap();
13326 assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
13327 }
13328
13329 #[test]
13330 fn test_parent_reference_resolves_to_enclosing_step_value() {
13331 let data: JValue = serde_json::json!({
13332 "Account": {
13333 "Order": [
13334 {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
13335 ]
13336 }
13337 })
13338 .into();
13339 let ast =
13340 crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
13341 .unwrap();
13342 let mut evaluator = Evaluator::new();
13343 let result = evaluator.evaluate(&ast, &data).unwrap();
13344 assert_eq!(
13345 result,
13346 serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
13347 );
13348 }
13349
13350 // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
13351 // a tuple-carried `$name`/`!label` key straight into the top scope and then
13352 // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
13353 // same-named outer `:=` binding that happened to be live in that scope
13354 // frame. Expected values below are verified against jsonata-js (2.2.1
13355 // reference, `tests/jsonata-js`).
13356
13357 #[test]
13358 fn test_chained_focus_bind_does_not_clobber_outer_variable() {
13359 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13360 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
13361 let mut evaluator = Evaluator::new();
13362 let result = evaluator.evaluate(&ast, &data).unwrap();
13363 assert_eq!(result, serde_json::json!("OUT").into());
13364 }
13365
13366 #[test]
13367 fn test_chained_index_bind_does_not_clobber_outer_variable() {
13368 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13369 let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
13370 let mut evaluator = Evaluator::new();
13371 let result = evaluator.evaluate(&ast, &data).unwrap();
13372 assert_eq!(result, serde_json::json!("OUT").into());
13373 }
13374
13375 #[test]
13376 fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
13377 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
13378 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
13379 let mut evaluator = Evaluator::new();
13380 let result = evaluator.evaluate(&ast, &data).unwrap();
13381 assert_eq!(result, serde_json::json!("OUT").into());
13382 }
13383
13384 #[test]
13385 fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
13386 let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
13387 let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
13388 let mut evaluator = Evaluator::new();
13389 let result = evaluator.evaluate(&ast, &data).unwrap();
13390 assert_eq!(result, serde_json::json!("OUT").into());
13391 }
13392}