jsonata_core/evaluator.rs
1// Expression evaluator
2// Mirrors jsonata.js from the reference implementation
3
4#![allow(clippy::cloned_ref_to_slice_refs)]
5#![allow(clippy::explicit_counter_loop)]
6#![allow(clippy::too_many_arguments)]
7#![allow(clippy::manual_strip)]
8
9use std::cmp::Ordering;
10use std::collections::{HashMap, HashSet};
11
12use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
13use crate::parser;
14use crate::value::JValue;
15use indexmap::IndexMap;
16use std::rc::Rc;
17use thiserror::Error;
18
19/// Specialized sort comparator for `$l.field op $r.field` patterns.
20/// Bypasses the full AST evaluator for simple field-based sort comparisons.
21///
22/// In JSONata `$sort`, the comparator returns true when `$l` should come AFTER `$r`.
23/// `$l.field > $r.field` swaps when left > right, producing ascending order.
24/// `$l.field < $r.field` swaps when left < right, producing descending order.
25struct SpecializedSortComparator {
26 field: String,
27 descending: bool,
28}
29
30/// Pre-extracted sort key for the Schwartzian transform in specialized sorting.
31enum SortKey {
32 Num(f64),
33 Str(Rc<str>),
34 None,
35}
36
37fn compare_sort_keys(a: &SortKey, b: &SortKey, descending: bool) -> Ordering {
38 let ord = match (a, b) {
39 (SortKey::Num(x), SortKey::Num(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
40 (SortKey::Str(x), SortKey::Str(y)) => (**x).cmp(&**y),
41 (SortKey::None, SortKey::None) => Ordering::Equal,
42 (SortKey::None, _) => Ordering::Greater,
43 (_, SortKey::None) => Ordering::Less,
44 // Mixed types: maintain original order
45 _ => Ordering::Equal,
46 };
47 if descending {
48 ord.reverse()
49 } else {
50 ord
51 }
52}
53
54/// Try to extract a specialized sort comparator from a lambda AST node.
55/// Detects patterns like `function($l, $r) { $l.field > $r.field }`.
56fn try_specialize_sort_comparator(
57 body: &AstNode,
58 left_param: &str,
59 right_param: &str,
60) -> Option<SpecializedSortComparator> {
61 let AstNode::Binary { op, lhs, rhs } = body else {
62 return None;
63 };
64
65 // Returns true if op means "swap when left > right" (ascending order).
66 let is_ascending = |op: &BinaryOp| -> Option<bool> {
67 match op {
68 BinaryOp::GreaterThan | BinaryOp::GreaterThanOrEqual => Some(true),
69 BinaryOp::LessThan | BinaryOp::LessThanOrEqual => Some(false),
70 _ => None,
71 }
72 };
73
74 // Extract field name from a `$param.field` path with no stages.
75 let extract_var_field = |node: &AstNode, param: &str| -> Option<String> {
76 let AstNode::Path { steps } = node else {
77 return None;
78 };
79 if steps.len() != 2 {
80 return None;
81 }
82 let AstNode::Variable(var) = &steps[0].node else {
83 return None;
84 };
85 if var != param {
86 return None;
87 }
88 let AstNode::Name(field) = &steps[1].node else {
89 return None;
90 };
91 if !steps[0].stages.is_empty() || !steps[1].stages.is_empty() {
92 return None;
93 }
94 Some(field.clone())
95 };
96
97 // Try both orientations: $l.field op $r.field and $r.field op $l.field (flipped).
98 for flipped in [false, true] {
99 let (lhs_param, rhs_param) = if flipped {
100 (right_param, left_param)
101 } else {
102 (left_param, right_param)
103 };
104 if let (Some(lhs_field), Some(rhs_field)) = (
105 extract_var_field(lhs, lhs_param),
106 extract_var_field(rhs, rhs_param),
107 ) {
108 if lhs_field == rhs_field {
109 let descending = match op {
110 // Subtraction: `$l.f - $r.f` → positive when l > r → ascending.
111 // Flipped `$r.f - $l.f` → positive when r > l → descending.
112 BinaryOp::Subtract => flipped,
113 // Comparison: `$l.f > $r.f` → ascending, flipped inverts.
114 _ => {
115 let ascending = is_ascending(op)?;
116 if flipped {
117 ascending
118 } else {
119 !ascending
120 }
121 }
122 };
123 return Some(SpecializedSortComparator {
124 field: lhs_field,
125 descending,
126 });
127 }
128 }
129 }
130 None
131}
132
133// ──────────────────────────────────────────────────────────────────────────────
134// CompiledExpr — unified compiled expression framework
135// ──────────────────────────────────────────────────────────────────────────────
136//
137// Generalizes SpecializedPredicate and CompiledObjectMap into a single IR that
138// can represent arbitrary simple expressions without AST walking. Evaluated in
139// a tight loop with no recursion tracking, no scope management, and no AstNode
140// pattern matching.
141
142/// Shape cache: maps field names to their positional index in an IndexMap.
143/// When all objects in an array share the same key ordering (extremely common
144/// in JSON data), field lookups become O(1) Vec index access via `get_index()`
145/// instead of O(1)-amortized hash lookups.
146type ShapeCache = HashMap<String, usize>;
147
148/// Build a shape cache from the first object in an array.
149/// Returns None if the data is not an object.
150fn build_shape_cache(first_element: &JValue) -> Option<ShapeCache> {
151 match first_element {
152 JValue::Object(obj) => {
153 let mut cache = HashMap::with_capacity(obj.len());
154 for (idx, (key, _)) in obj.iter().enumerate() {
155 cache.insert(key.clone(), idx);
156 }
157 Some(cache)
158 }
159 _ => None,
160 }
161}
162
163/// Comparison operator for compiled expressions.
164#[derive(Debug, Clone, Copy)]
165pub(crate) enum CompiledCmp {
166 Eq,
167 Ne,
168 Lt,
169 Le,
170 Gt,
171 Ge,
172}
173
174/// Arithmetic operator for compiled expressions.
175#[derive(Debug, Clone, Copy)]
176pub(crate) enum CompiledArithOp {
177 Add,
178 Sub,
179 Mul,
180 Div,
181 Mod,
182}
183
184/// Unified compiled expression — replaces SpecializedPredicate & CompiledObjectMap.
185///
186/// `try_compile_expr()` converts an AstNode subtree into a CompiledExpr at
187/// expression-compile time (once), then `eval_compiled()` evaluates it per
188/// element in O(expression-size) with no heap allocation in the hot path.
189#[derive(Clone, Debug)]
190pub(crate) enum CompiledExpr {
191 // ── Leaves ──────────────────────────────────────────────────────────
192 /// A literal value known at compile time.
193 Literal(JValue),
194 /// Explicit `null` literal from `AstNode::Null`.
195 /// Distinct from field-lookup-produced null: triggers T2010/T2002 errors
196 /// in comparisons/arithmetic, matching the tree-walker's `explicit_null` semantics.
197 ExplicitNull,
198 /// Single-level field lookup on the current object: `obj.get("field")`.
199 FieldLookup(String),
200 /// Two-level nested field lookup: `obj.get("a")?.get("b")`.
201 NestedFieldLookup(String, String),
202 /// Variable lookup from enclosing scope (e.g. `$var`).
203 /// Resolved at eval time via a provided variable map.
204 VariableLookup(String),
205
206 // ── Comparison ──────────────────────────────────────────────────────
207 Compare {
208 op: CompiledCmp,
209 lhs: Box<CompiledExpr>,
210 rhs: Box<CompiledExpr>,
211 },
212
213 // ── Arithmetic ──────────────────────────────────────────────────────
214 Arithmetic {
215 op: CompiledArithOp,
216 lhs: Box<CompiledExpr>,
217 rhs: Box<CompiledExpr>,
218 },
219
220 // ── String ──────────────────────────────────────────────────────────
221 Concat(Box<CompiledExpr>, Box<CompiledExpr>),
222
223 // ── Logical ─────────────────────────────────────────────────────────
224 And(Box<CompiledExpr>, Box<CompiledExpr>),
225 Or(Box<CompiledExpr>, Box<CompiledExpr>),
226 Not(Box<CompiledExpr>),
227 /// Negation of a numeric value.
228 Negate(Box<CompiledExpr>),
229
230 // ── Conditional ─────────────────────────────────────────────────────
231 Conditional {
232 condition: Box<CompiledExpr>,
233 then_expr: Box<CompiledExpr>,
234 else_expr: Option<Box<CompiledExpr>>,
235 },
236
237 // ── Compound ────────────────────────────────────────────────────────
238 /// Object construction: `{"key1": expr1, "key2": expr2, ...}`
239 ObjectConstruct(Vec<(String, CompiledExpr)>),
240 /// Array construction: `[expr1, expr2, ...]`
241 ///
242 /// Each element carries a `bool` flag: `true` means the element originated
243 /// from an explicit `AstNode::Array` constructor and must be kept nested even
244 /// if it evaluates to an array. `false` means the element's array value is
245 /// flattened one level into the outer result (JSONata `[a.b, ...]` semantics).
246 /// Undefined values are always skipped.
247 ArrayConstruct(Vec<(CompiledExpr, bool)>),
248
249 // ── Phase 2 extensions ──────────────────────────────────────────────
250 /// Named variable lookup from context scope (any `$name` not in lambda params).
251 /// Compiled when a named variable is encountered and no allowed_vars list is
252 /// provided (top-level compilation). At runtime, returns the value from the vars
253 /// map (lambda params or captured env), or Undefined if not present.
254 #[allow(dead_code)]
255 ContextVar(String),
256
257 /// Multi-step field path with optional per-step filters: `a.b[pred].c`
258 /// Applies implicit array-mapping semantics at each step.
259 FieldPath(Vec<CompiledStep>),
260
261 /// Call a pure, side-effect-free builtin with compiled arguments.
262 /// Only builtins in COMPILABLE_BUILTINS are allowed here.
263 BuiltinCall {
264 name: &'static str,
265 args: Vec<CompiledExpr>,
266 },
267
268 /// Sequential block: evaluate all expressions, return last value.
269 Block(Vec<CompiledExpr>),
270
271 /// Coalesce (`??`): return lhs if it is defined and non-null, else rhs.
272 Coalesce(Box<CompiledExpr>, Box<CompiledExpr>),
273
274 // ── Higher-order functions with inline lambdas ───────────────────────
275 /// `$map(array, function($v [, $i]) { body })` — compiled when the second
276 /// argument is an inline lambda literal (not a stored variable).
277 /// `params` holds the lambda parameter names (without `$`), 1 or 2 elements.
278 MapCall {
279 array: Box<CompiledExpr>,
280 params: Vec<String>,
281 body: Box<CompiledExpr>,
282 },
283 /// `$filter(array, function($v [, $i]) { body })` — compiled when the second
284 /// argument is an inline lambda literal.
285 FilterCall {
286 array: Box<CompiledExpr>,
287 params: Vec<String>,
288 body: Box<CompiledExpr>,
289 },
290 /// `$reduce(array, function($acc, $v) { body } [, initial])` — compiled when the
291 /// second argument is an inline lambda literal with exactly 2 parameters.
292 ReduceCall {
293 array: Box<CompiledExpr>,
294 params: Vec<String>,
295 body: Box<CompiledExpr>,
296 initial: Option<Box<CompiledExpr>>,
297 },
298}
299
300/// One step in a compiled `FieldPath`.
301#[derive(Clone, Debug)]
302pub(crate) struct CompiledStep {
303 /// Field name to look up at this step.
304 pub field: String,
305 /// Optional predicate filter compiled from a `Stage::Filter` stage.
306 pub filter: Option<CompiledExpr>,
307}
308
309/// Try to compile an AstNode subtree into a CompiledExpr.
310/// Returns None for anything that requires full AST evaluation (lambda calls,
311/// function calls with side effects, complex paths, etc.).
312pub(crate) fn try_compile_expr(node: &AstNode) -> Option<CompiledExpr> {
313 try_compile_expr_inner(node, None)
314}
315
316/// Like `try_compile_expr` but additionally allows the specified variable names
317/// to be compiled as `VariableLookup`. Used by HOF integration where lambda
318/// parameters are known and will be provided via the `vars` map at eval time.
319pub(crate) fn try_compile_expr_with_allowed_vars(
320 node: &AstNode,
321 allowed_vars: &[&str],
322) -> Option<CompiledExpr> {
323 try_compile_expr_inner(node, Some(allowed_vars))
324}
325
326fn try_compile_expr_inner(node: &AstNode, allowed_vars: Option<&[&str]>) -> Option<CompiledExpr> {
327 match node {
328 // ── Literals ────────────────────────────────────────────────────
329 AstNode::String(s) => Some(CompiledExpr::Literal(JValue::string(s.clone()))),
330 AstNode::Number(n) => Some(CompiledExpr::Literal(JValue::Number(*n))),
331 AstNode::Boolean(b) => Some(CompiledExpr::Literal(JValue::Bool(*b))),
332 AstNode::Null => Some(CompiledExpr::ExplicitNull),
333
334 // ── Field access ────────────────────────────────────────────────
335 AstNode::Name(field) => Some(CompiledExpr::FieldLookup(field.clone())),
336
337 // ── Variable lookup ─────────────────────────────────────────────
338 // $ (empty name) always refers to the current element.
339 // Named variables: in HOF mode (allowed_vars=Some), only compile if the
340 // variable is in the allowed set (lambda params supplied via vars map).
341 // In top-level mode (allowed_vars=None), compile unknown variables as
342 // ContextVar — they return Undefined at runtime when no bindings are passed.
343 AstNode::Variable(var) if var.is_empty() => Some(CompiledExpr::VariableLookup(var.clone())),
344 AstNode::Variable(var) => {
345 if let Some(allowed) = allowed_vars {
346 // HOF mode: only compile if the variable is a known lambda param.
347 if allowed.contains(&var.as_str()) {
348 return Some(CompiledExpr::VariableLookup(var.clone()));
349 }
350 }
351 // Named variables require Context for correct lookup (scope stack, builtins
352 // registry). The compiled fast path passes ctx=None, so fall back to the
353 // tree-walker for all non-empty variable references.
354 None
355 }
356
357 // ── Path expressions ────────────────────────────────────────────
358 AstNode::Path { steps } => try_compile_path(steps, allowed_vars),
359
360 // ── Binary operations ───────────────────────────────────────────
361 AstNode::Binary { op, lhs, rhs } => {
362 let compiled_lhs = try_compile_expr_inner(lhs, allowed_vars)?;
363 let compiled_rhs = try_compile_expr_inner(rhs, allowed_vars)?;
364 match op {
365 // Comparison
366 BinaryOp::Equal => Some(CompiledExpr::Compare {
367 op: CompiledCmp::Eq,
368 lhs: Box::new(compiled_lhs),
369 rhs: Box::new(compiled_rhs),
370 }),
371 BinaryOp::NotEqual => Some(CompiledExpr::Compare {
372 op: CompiledCmp::Ne,
373 lhs: Box::new(compiled_lhs),
374 rhs: Box::new(compiled_rhs),
375 }),
376 BinaryOp::LessThan => Some(CompiledExpr::Compare {
377 op: CompiledCmp::Lt,
378 lhs: Box::new(compiled_lhs),
379 rhs: Box::new(compiled_rhs),
380 }),
381 BinaryOp::LessThanOrEqual => Some(CompiledExpr::Compare {
382 op: CompiledCmp::Le,
383 lhs: Box::new(compiled_lhs),
384 rhs: Box::new(compiled_rhs),
385 }),
386 BinaryOp::GreaterThan => Some(CompiledExpr::Compare {
387 op: CompiledCmp::Gt,
388 lhs: Box::new(compiled_lhs),
389 rhs: Box::new(compiled_rhs),
390 }),
391 BinaryOp::GreaterThanOrEqual => Some(CompiledExpr::Compare {
392 op: CompiledCmp::Ge,
393 lhs: Box::new(compiled_lhs),
394 rhs: Box::new(compiled_rhs),
395 }),
396 // Arithmetic
397 BinaryOp::Add => Some(CompiledExpr::Arithmetic {
398 op: CompiledArithOp::Add,
399 lhs: Box::new(compiled_lhs),
400 rhs: Box::new(compiled_rhs),
401 }),
402 BinaryOp::Subtract => Some(CompiledExpr::Arithmetic {
403 op: CompiledArithOp::Sub,
404 lhs: Box::new(compiled_lhs),
405 rhs: Box::new(compiled_rhs),
406 }),
407 BinaryOp::Multiply => Some(CompiledExpr::Arithmetic {
408 op: CompiledArithOp::Mul,
409 lhs: Box::new(compiled_lhs),
410 rhs: Box::new(compiled_rhs),
411 }),
412 BinaryOp::Divide => Some(CompiledExpr::Arithmetic {
413 op: CompiledArithOp::Div,
414 lhs: Box::new(compiled_lhs),
415 rhs: Box::new(compiled_rhs),
416 }),
417 BinaryOp::Modulo => Some(CompiledExpr::Arithmetic {
418 op: CompiledArithOp::Mod,
419 lhs: Box::new(compiled_lhs),
420 rhs: Box::new(compiled_rhs),
421 }),
422 // Logical
423 BinaryOp::And => Some(CompiledExpr::And(
424 Box::new(compiled_lhs),
425 Box::new(compiled_rhs),
426 )),
427 BinaryOp::Or => Some(CompiledExpr::Or(
428 Box::new(compiled_lhs),
429 Box::new(compiled_rhs),
430 )),
431 // String concat
432 BinaryOp::Concatenate => Some(CompiledExpr::Concat(
433 Box::new(compiled_lhs),
434 Box::new(compiled_rhs),
435 )),
436 // Coalesce: return lhs if defined/non-null, else rhs
437 BinaryOp::Coalesce => Some(CompiledExpr::Coalesce(
438 Box::new(compiled_lhs),
439 Box::new(compiled_rhs),
440 )),
441 // Anything else (Range, In, ColonEqual, ChainPipe, etc.) — not compilable
442 _ => None,
443 }
444 }
445
446 // ── Unary operations ────────────────────────────────────────────
447 AstNode::Unary { op, operand } => {
448 let compiled = try_compile_expr_inner(operand, allowed_vars)?;
449 match op {
450 crate::ast::UnaryOp::Not => Some(CompiledExpr::Not(Box::new(compiled))),
451 crate::ast::UnaryOp::Negate => Some(CompiledExpr::Negate(Box::new(compiled))),
452 }
453 }
454
455 // ── Conditional ─────────────────────────────────────────────────
456 AstNode::Conditional {
457 condition,
458 then_branch,
459 else_branch,
460 } => {
461 let cond = try_compile_expr_inner(condition, allowed_vars)?;
462 let then_e = try_compile_expr_inner(then_branch, allowed_vars)?;
463 let else_e = match else_branch {
464 Some(e) => Some(Box::new(try_compile_expr_inner(e, allowed_vars)?)),
465 None => None,
466 };
467 Some(CompiledExpr::Conditional {
468 condition: Box::new(cond),
469 then_expr: Box::new(then_e),
470 else_expr: else_e,
471 })
472 }
473
474 // ── Object construction ─────────────────────────────────────────
475 AstNode::Object(pairs) => {
476 let mut fields = Vec::with_capacity(pairs.len());
477 for (key_node, val_node) in pairs {
478 // Key must be a string literal
479 let key = match key_node {
480 AstNode::String(s) => s.clone(),
481 _ => return None,
482 };
483 let val = try_compile_expr_inner(val_node, allowed_vars)?;
484 fields.push((key, val));
485 }
486 Some(CompiledExpr::ObjectConstruct(fields))
487 }
488
489 // ── Array construction ──────────────────────────────────────────
490 AstNode::Array(elems) => {
491 let mut compiled = Vec::with_capacity(elems.len());
492 for elem in elems {
493 // Tag whether the element itself is an array constructor: if so, its
494 // array value must be kept nested rather than flattened (tree-walker parity).
495 let is_nested = matches!(elem, AstNode::Array(_));
496 compiled.push((try_compile_expr_inner(elem, allowed_vars)?, is_nested));
497 }
498 Some(CompiledExpr::ArrayConstruct(compiled))
499 }
500
501 // ── Block (sequential evaluation) ───────────────────────────────
502 AstNode::Block(exprs) if !exprs.is_empty() => {
503 let compiled: Option<Vec<CompiledExpr>> = exprs
504 .iter()
505 .map(|e| try_compile_expr_inner(e, allowed_vars))
506 .collect();
507 compiled.map(CompiledExpr::Block)
508 }
509
510 // ── Pure builtin function calls ──────────────────────────────────
511 AstNode::Function {
512 name,
513 args,
514 is_builtin: true,
515 } => {
516 if is_compilable_builtin(name) {
517 // Arity guard: if the call site passes more args than the builtin accepts,
518 // fall back to the tree-walker so it can raise the correct T0410 error.
519 if let Some(max) = compilable_builtin_max_args(name) {
520 if args.len() > max {
521 return None;
522 }
523 }
524 let compiled_args: Option<Vec<CompiledExpr>> = args
525 .iter()
526 .map(|a| try_compile_expr_inner(a, allowed_vars))
527 .collect();
528 compiled_args.map(|cargs| CompiledExpr::BuiltinCall {
529 name: static_builtin_name(name),
530 args: cargs,
531 })
532 } else {
533 try_compile_hof_expr(name, args, allowed_vars)
534 }
535 }
536
537 // Everything else: Lambda, non-pure builtins, Sort, Transform, etc.
538 _ => None,
539 }
540}
541
542/// Extract an inline lambda's params and body from an AST node, returning `None` if the
543/// node is not a simple lambda (i.e. has a signature or is a TCO thunk).
544fn extract_inline_lambda(node: &AstNode) -> Option<(&Vec<String>, &AstNode)> {
545 match node {
546 AstNode::Lambda {
547 params,
548 body,
549 signature: None,
550 thunk: false,
551 } => Some((params, body)),
552 _ => None,
553 }
554}
555
556/// Compile the array argument + lambda body for a HOF call, returning `None` if either
557/// fails to compile. The lambda params are added to the allowed-vars set so the body
558/// can reference them.
559fn compile_hof_array_and_body(
560 array_node: &AstNode,
561 params: &[String],
562 body: &AstNode,
563 allowed_vars: Option<&[&str]>,
564) -> Option<(Box<CompiledExpr>, Box<CompiledExpr>)> {
565 let array = try_compile_expr_inner(array_node, allowed_vars)?;
566 let param_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
567 let compiled_body = try_compile_expr_inner(body, Some(¶m_refs))?;
568 Some((Box::new(array), Box::new(compiled_body)))
569}
570
571/// Try to compile a higher-order function call (`$map`, `$filter`, `$reduce`) when the
572/// callback argument is an inline lambda literal with a compilable body.
573///
574/// Returns `None` when:
575/// - The callback is not an inline lambda (e.g. a stored variable `$f`) — fall back so
576/// the tree-walker can look up the lambda at runtime.
577/// - The lambda has a signature or is a TCO thunk — semantics require the full evaluator.
578/// - The lambda body is not fully compilable — fall back transparently.
579/// - Param count is outside the supported range (see per-function constraints below).
580fn try_compile_hof_expr(
581 name: &str,
582 args: &[AstNode],
583 allowed_vars: Option<&[&str]>,
584) -> Option<CompiledExpr> {
585 match name {
586 "map" | "filter" => {
587 if args.len() != 2 {
588 return None;
589 }
590 let (params, body) = extract_inline_lambda(&args[1])?;
591 if params.is_empty() || params.len() > 2 {
592 return None;
593 }
594 let (array, compiled_body) =
595 compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
596 if name == "map" {
597 Some(CompiledExpr::MapCall {
598 array,
599 params: params.clone(),
600 body: compiled_body,
601 })
602 } else {
603 Some(CompiledExpr::FilterCall {
604 array,
605 params: params.clone(),
606 body: compiled_body,
607 })
608 }
609 }
610 "reduce" => {
611 if args.len() < 2 || args.len() > 3 {
612 return None;
613 }
614 let (params, body) = extract_inline_lambda(&args[1])?;
615 if params.len() != 2 {
616 return None;
617 }
618 let (array, compiled_body) =
619 compile_hof_array_and_body(&args[0], params, body, allowed_vars)?;
620 let initial = if args.len() == 3 {
621 Some(Box::new(try_compile_expr_inner(&args[2], allowed_vars)?))
622 } else {
623 None
624 };
625 Some(CompiledExpr::ReduceCall {
626 array,
627 params: params.clone(),
628 body: compiled_body,
629 initial,
630 })
631 }
632 _ => None,
633 }
634}
635
636/// Returns true if the named builtin is pure (no side effects, no context dependency)
637/// and can be safely compiled into a BuiltinCall.
638fn is_compilable_builtin(name: &str) -> bool {
639 matches!(
640 name,
641 "string"
642 | "length"
643 | "substring"
644 | "substringBefore"
645 | "substringAfter"
646 | "uppercase"
647 | "lowercase"
648 | "trim"
649 | "contains"
650 | "split"
651 | "join"
652 | "number"
653 | "floor"
654 | "ceil"
655 | "round"
656 | "abs"
657 | "sqrt"
658 | "sum"
659 | "max"
660 | "min"
661 | "average"
662 | "count"
663 | "boolean"
664 | "not"
665 | "keys"
666 | "append"
667 | "reverse"
668 | "distinct"
669 | "merge"
670 )
671}
672
673/// Maximum number of explicit arguments accepted by each compilable builtin.
674/// Returns `None` for variadic functions with no fixed upper bound.
675/// Used at compile time to fall back to the tree-walker for over-arity calls
676/// (which the tree-walker turns into the correct T0410/T0411 type errors).
677fn compilable_builtin_max_args(name: &str) -> Option<usize> {
678 match name {
679 "string" => Some(2),
680 "length" | "uppercase" | "lowercase" | "trim" => Some(1),
681 "substring" | "split" => Some(3),
682 "substringBefore" | "substringAfter" | "contains" | "join" | "append" | "round" => Some(2),
683 "number" | "floor" | "ceil" | "abs" | "sqrt" => Some(1),
684 "sum" | "max" | "min" | "average" | "count" => Some(1),
685 "boolean" | "not" | "keys" | "reverse" | "distinct" => Some(1),
686 "merge" => None, // variadic: $merge(obj1, obj2, …) or $merge([…])
687 _ => None,
688 }
689}
690
691/// Return the `&'static str` for a known compilable builtin name.
692/// SAFETY: only called after `is_compilable_builtin` returns true.
693fn static_builtin_name(name: &str) -> &'static str {
694 match name {
695 "string" => "string",
696 "length" => "length",
697 "substring" => "substring",
698 "substringBefore" => "substringBefore",
699 "substringAfter" => "substringAfter",
700 "uppercase" => "uppercase",
701 "lowercase" => "lowercase",
702 "trim" => "trim",
703 "contains" => "contains",
704 "split" => "split",
705 "join" => "join",
706 "number" => "number",
707 "floor" => "floor",
708 "ceil" => "ceil",
709 "round" => "round",
710 "abs" => "abs",
711 "sqrt" => "sqrt",
712 "sum" => "sum",
713 "max" => "max",
714 "min" => "min",
715 "average" => "average",
716 "count" => "count",
717 "boolean" => "boolean",
718 "not" => "not",
719 "keys" => "keys",
720 "append" => "append",
721 "reverse" => "reverse",
722 "distinct" => "distinct",
723 "merge" => "merge",
724 _ => unreachable!("Not a compilable builtin: {}", name),
725 }
726}
727
728/// Evaluate a compiled expression against a single element.
729///
730/// `data` is the current element (typically an object from an array).
731/// `vars` is an optional map of variable bindings (for HOF lambda parameters).
732///
733/// This is the tight inner loop — no recursion tracking, no scope push/pop,
734/// no AstNode pattern matching.
735#[inline(always)]
736pub(crate) fn eval_compiled(
737 expr: &CompiledExpr,
738 data: &JValue,
739 vars: Option<&HashMap<&str, &JValue>>,
740) -> Result<JValue, EvaluatorError> {
741 eval_compiled_inner(expr, data, vars, None, None)
742}
743
744/// Like `eval_compiled` but with an optional shape cache for O(1) positional
745/// field access. The shape cache maps field names to their index in the object's
746/// internal Vec, enabling `get_index()` instead of hash lookups.
747#[inline(always)]
748fn eval_compiled_shaped(
749 expr: &CompiledExpr,
750 data: &JValue,
751 vars: Option<&HashMap<&str, &JValue>>,
752 shape: &ShapeCache,
753) -> Result<JValue, EvaluatorError> {
754 eval_compiled_inner(expr, data, vars, None, Some(shape))
755}
756
757/// Clone the outer variable bindings into a new HashMap with the given capacity hint.
758/// Used by HOF eval arms to create per-iteration variable scopes that merge outer vars
759/// with lambda parameters.
760#[inline]
761fn clone_outer_vars<'a>(
762 vars: Option<&HashMap<&'a str, &'a JValue>>,
763 capacity: usize,
764) -> HashMap<&'a str, &'a JValue> {
765 vars.map(|v| v.iter().map(|(&k, v)| (k, *v)).collect())
766 .unwrap_or_else(|| HashMap::with_capacity(capacity))
767}
768
769fn eval_compiled_inner(
770 expr: &CompiledExpr,
771 data: &JValue,
772 vars: Option<&HashMap<&str, &JValue>>,
773 ctx: Option<&Context>,
774 shape: Option<&ShapeCache>,
775) -> Result<JValue, EvaluatorError> {
776 match expr {
777 // ── Leaves ──────────────────────────────────────────────────────
778 CompiledExpr::Literal(v) => Ok(v.clone()),
779
780 // ExplicitNull evaluates to Null, but is flagged at compile-time for
781 // comparison/arithmetic arms to trigger the correct T2010/T2002 errors.
782 CompiledExpr::ExplicitNull => Ok(JValue::Null),
783
784 CompiledExpr::FieldLookup(field) => match data {
785 JValue::Object(obj) => {
786 // Shape-accelerated: use positional index if available
787 if let Some(shape) = shape {
788 if let Some(&idx) = shape.get(field.as_str()) {
789 return Ok(obj
790 .get_index(idx)
791 .map(|(_, v)| v.clone())
792 .unwrap_or(JValue::Undefined));
793 }
794 }
795 Ok(obj
796 .get(field.as_str())
797 .cloned()
798 .unwrap_or(JValue::Undefined))
799 }
800 _ => Ok(JValue::Undefined),
801 },
802
803 CompiledExpr::NestedFieldLookup(outer, inner) => match data {
804 JValue::Object(obj) => {
805 // Shape-accelerated outer lookup
806 let outer_val = if let Some(shape) = shape {
807 if let Some(&idx) = shape.get(outer.as_str()) {
808 obj.get_index(idx).map(|(_, v)| v)
809 } else {
810 obj.get(outer.as_str())
811 }
812 } else {
813 obj.get(outer.as_str())
814 };
815 Ok(outer_val
816 .and_then(|v| match v {
817 JValue::Object(nested) => nested.get(inner.as_str()).cloned(),
818 _ => None,
819 })
820 .unwrap_or(JValue::Undefined))
821 }
822 _ => Ok(JValue::Undefined),
823 },
824
825 CompiledExpr::VariableLookup(var) => {
826 if let Some(vars) = vars {
827 if let Some(val) = vars.get(var.as_str()) {
828 return Ok((*val).clone());
829 }
830 }
831 // $ (empty var name) refers to the current data
832 if var.is_empty() {
833 return Ok(data.clone());
834 }
835 Ok(JValue::Undefined)
836 }
837
838 // ── Comparison ──────────────────────────────────────────────────
839 CompiledExpr::Compare { op, lhs, rhs } => {
840 let lhs_explicit_null = is_compiled_explicit_null(lhs);
841 let rhs_explicit_null = is_compiled_explicit_null(rhs);
842 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
843 let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
844 match op {
845 CompiledCmp::Eq => Ok(JValue::Bool(crate::functions::array::values_equal(
846 &left, &right,
847 ))),
848 CompiledCmp::Ne => Ok(JValue::Bool(!crate::functions::array::values_equal(
849 &left, &right,
850 ))),
851 CompiledCmp::Lt => compiled_ordered_cmp(
852 &left,
853 &right,
854 lhs_explicit_null,
855 rhs_explicit_null,
856 |a, b| a < b,
857 |a, b| a < b,
858 ),
859 CompiledCmp::Le => compiled_ordered_cmp(
860 &left,
861 &right,
862 lhs_explicit_null,
863 rhs_explicit_null,
864 |a, b| a <= b,
865 |a, b| a <= b,
866 ),
867 CompiledCmp::Gt => compiled_ordered_cmp(
868 &left,
869 &right,
870 lhs_explicit_null,
871 rhs_explicit_null,
872 |a, b| a > b,
873 |a, b| a > b,
874 ),
875 CompiledCmp::Ge => compiled_ordered_cmp(
876 &left,
877 &right,
878 lhs_explicit_null,
879 rhs_explicit_null,
880 |a, b| a >= b,
881 |a, b| a >= b,
882 ),
883 }
884 }
885
886 // ── Arithmetic ──────────────────────────────────────────────────
887 CompiledExpr::Arithmetic { op, lhs, rhs } => {
888 let lhs_explicit_null = is_compiled_explicit_null(lhs);
889 let rhs_explicit_null = is_compiled_explicit_null(rhs);
890 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
891 let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
892 compiled_arithmetic(*op, &left, &right, lhs_explicit_null, rhs_explicit_null)
893 }
894
895 // ── String concat ───────────────────────────────────────────────
896 CompiledExpr::Concat(lhs, rhs) => {
897 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
898 let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
899 let ls = compiled_to_concat_string(&left)?;
900 let rs = compiled_to_concat_string(&right)?;
901 Ok(JValue::string(format!("{}{}", ls, rs)))
902 }
903
904 // ── Logical ─────────────────────────────────────────────────────
905 CompiledExpr::And(lhs, rhs) => {
906 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
907 if !compiled_is_truthy(&left) {
908 return Ok(JValue::Bool(false));
909 }
910 let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
911 Ok(JValue::Bool(compiled_is_truthy(&right)))
912 }
913 CompiledExpr::Or(lhs, rhs) => {
914 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
915 if compiled_is_truthy(&left) {
916 return Ok(JValue::Bool(true));
917 }
918 let right = eval_compiled_inner(rhs, data, vars, ctx, shape)?;
919 Ok(JValue::Bool(compiled_is_truthy(&right)))
920 }
921 CompiledExpr::Not(inner) => {
922 let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
923 Ok(JValue::Bool(!compiled_is_truthy(&val)))
924 }
925 CompiledExpr::Negate(inner) => {
926 let val = eval_compiled_inner(inner, data, vars, ctx, shape)?;
927 match val {
928 JValue::Number(n) => Ok(JValue::Number(-n)),
929 JValue::Null => Ok(JValue::Null),
930 // Undefined operand propagates through unary minus, matching the tree-walker.
931 v if v.is_undefined() => Ok(JValue::Undefined),
932 _ => Err(EvaluatorError::TypeError(
933 "D1002: Cannot negate non-number value".to_string(),
934 )),
935 }
936 }
937
938 // ── Conditional ─────────────────────────────────────────────────
939 CompiledExpr::Conditional {
940 condition,
941 then_expr,
942 else_expr,
943 } => {
944 let cond = eval_compiled_inner(condition, data, vars, ctx, shape)?;
945 if compiled_is_truthy(&cond) {
946 eval_compiled_inner(then_expr, data, vars, ctx, shape)
947 } else if let Some(else_e) = else_expr {
948 eval_compiled_inner(else_e, data, vars, ctx, shape)
949 } else {
950 Ok(JValue::Undefined)
951 }
952 }
953
954 // ── Object construction ─────────────────────────────────────────
955 CompiledExpr::ObjectConstruct(fields) => {
956 let mut result = IndexMap::with_capacity(fields.len());
957 for (key, expr) in fields {
958 let value = eval_compiled_inner(expr, data, vars, ctx, shape)?;
959 if !value.is_undefined() {
960 result.insert(key.clone(), value);
961 }
962 }
963 Ok(JValue::object(result))
964 }
965
966 // ── Array construction ──────────────────────────────────────────
967 CompiledExpr::ArrayConstruct(elems) => {
968 let mut result = Vec::new();
969 for (elem_expr, is_nested) in elems {
970 let value = eval_compiled_inner(elem_expr, data, vars, ctx, shape)?;
971 // Undefined values are excluded from array constructors (tree-walker parity)
972 if value.is_undefined() {
973 continue;
974 }
975 if *is_nested {
976 // Explicit array constructor [...] — keep nested even if it's an array
977 result.push(value);
978 } else if let JValue::Array(arr) = value {
979 // Non-constructor that evaluated to an array — flatten one level
980 result.extend(arr.iter().cloned());
981 } else {
982 result.push(value);
983 }
984 }
985 Ok(JValue::array(result))
986 }
987
988 // ── Phase 2 new variants ─────────────────────────────────────────
989
990 // ContextVar: named variable lookup from context scope.
991 // In top-level mode (ctx=None, no bindings), returns Undefined.
992 // In HOF mode, ctx is None too (HOF call sites pass no ctx), so this
993 // is only ever populated for top-level calls — always Undefined there.
994 CompiledExpr::ContextVar(name) => {
995 // Check vars map first (for lambda params that might shadow context)
996 if let Some(vars) = vars {
997 if let Some(val) = vars.get(name.as_str()) {
998 return Ok((*val).clone());
999 }
1000 }
1001 // Then check context scope
1002 if let Some(ctx) = ctx {
1003 if let Some(val) = ctx.lookup(name) {
1004 return Ok(val.clone());
1005 }
1006 }
1007 Ok(JValue::Undefined)
1008 }
1009
1010 // FieldPath: multi-step field access with implicit array mapping.
1011 CompiledExpr::FieldPath(steps) => compiled_eval_field_path(steps, data, vars, ctx, shape),
1012
1013 // BuiltinCall: evaluate all args, dispatch to pure builtin.
1014 CompiledExpr::BuiltinCall { name, args } => {
1015 let mut evaled_args = Vec::with_capacity(args.len());
1016 for arg in args.iter() {
1017 evaled_args.push(eval_compiled_inner(arg, data, vars, ctx, shape)?);
1018 }
1019 call_pure_builtin(name, &evaled_args, data)
1020 }
1021
1022 // Block: evaluate each expression in sequence, return the last value.
1023 CompiledExpr::Block(exprs) => {
1024 let mut result = JValue::Undefined;
1025 for expr in exprs.iter() {
1026 result = eval_compiled_inner(expr, data, vars, ctx, shape)?;
1027 }
1028 Ok(result)
1029 }
1030
1031 // Coalesce (`??`): return lhs unless it is Undefined; null IS a valid value.
1032 // JSONata spec: "returns the RHS operand if the LHS operand evaluates to undefined".
1033 CompiledExpr::Coalesce(lhs, rhs) => {
1034 let left = eval_compiled_inner(lhs, data, vars, ctx, shape)?;
1035 if left.is_undefined() {
1036 eval_compiled_inner(rhs, data, vars, ctx, shape)
1037 } else {
1038 Ok(left)
1039 }
1040 }
1041
1042 // ── Higher-order functions ─────────────────────────────────────────────
1043 //
1044 // These variants are emitted by try_compile_hof_expr when the HOF argument
1045 // is an inline lambda literal with a compilable body. Outer vars are merged
1046 // with the lambda params so that nested HOF can access variables from
1047 // enclosing lambda scopes (e.g. `$map(a, function($x) { $map(b, function($y) { $x + $y }) })`).
1048 CompiledExpr::MapCall {
1049 array,
1050 params,
1051 body,
1052 } => {
1053 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1054 let single_holder;
1055 let items: &[JValue] = match &arr_val {
1056 JValue::Array(a) => a.as_slice(),
1057 JValue::Undefined => return Ok(JValue::Undefined),
1058 other => {
1059 single_holder = [other.clone()];
1060 &single_holder[..]
1061 }
1062 };
1063 let mut result = Vec::with_capacity(items.len());
1064 let p0 = params.first().map(|s| s.as_str());
1065
1066 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1067 // 2-param lambda (element + index): build per-iteration because idx_val
1068 // is loop-local and cannot outlive the iteration.
1069 for (idx, item) in items.iter().enumerate() {
1070 let idx_val = JValue::Number(idx as f64);
1071 let mut call_vars = clone_outer_vars(vars, 2);
1072 if let Some(p) = p0 {
1073 call_vars.insert(p, item);
1074 }
1075 call_vars.insert(p1, &idx_val);
1076 let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1077 if !mapped.is_undefined() {
1078 result.push(mapped);
1079 }
1080 }
1081 } else if let Some(p0) = p0 {
1082 // 1-param lambda (most common): build HashMap once, update element ref each iteration.
1083 let mut call_vars = clone_outer_vars(vars, 1);
1084 for item in items.iter() {
1085 call_vars.insert(p0, item);
1086 let mapped = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1087 if !mapped.is_undefined() {
1088 result.push(mapped);
1089 }
1090 }
1091 }
1092 Ok(if result.is_empty() {
1093 JValue::Undefined
1094 } else {
1095 JValue::array(result)
1096 })
1097 }
1098
1099 CompiledExpr::FilterCall {
1100 array,
1101 params,
1102 body,
1103 } => {
1104 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1105 if arr_val.is_undefined() || arr_val.is_null() {
1106 return Ok(JValue::Undefined);
1107 }
1108 let single_holder;
1109 let (items, was_single) = match &arr_val {
1110 JValue::Array(a) => (a.as_slice(), false),
1111 other => {
1112 single_holder = [other.clone()];
1113 (&single_holder[..], true)
1114 }
1115 };
1116 let mut result = Vec::with_capacity(items.len() / 2);
1117 let p0 = params.first().map(|s| s.as_str());
1118
1119 if let Some(p1) = params.get(1).map(|s| s.as_str()) {
1120 for (idx, item) in items.iter().enumerate() {
1121 let idx_val = JValue::Number(idx as f64);
1122 let mut call_vars = clone_outer_vars(vars, 2);
1123 if let Some(p) = p0 {
1124 call_vars.insert(p, item);
1125 }
1126 call_vars.insert(p1, &idx_val);
1127 let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1128 if compiled_is_truthy(&pred) {
1129 result.push(item.clone());
1130 }
1131 }
1132 } else if let Some(p0) = p0 {
1133 let mut call_vars = clone_outer_vars(vars, 1);
1134 for item in items.iter() {
1135 call_vars.insert(p0, item);
1136 let pred = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1137 if compiled_is_truthy(&pred) {
1138 result.push(item.clone());
1139 }
1140 }
1141 }
1142 if was_single {
1143 Ok(match result.len() {
1144 0 => JValue::Undefined,
1145 1 => result.remove(0),
1146 _ => JValue::array(result),
1147 })
1148 } else {
1149 Ok(JValue::array(result))
1150 }
1151 }
1152
1153 CompiledExpr::ReduceCall {
1154 array,
1155 params,
1156 body,
1157 initial,
1158 } => {
1159 let arr_val = eval_compiled_inner(array, data, vars, ctx, shape)?;
1160 let single_holder;
1161 let items: &[JValue] = match &arr_val {
1162 JValue::Array(a) => a.as_slice(),
1163 JValue::Null => return Ok(JValue::Null),
1164 JValue::Undefined => return Ok(JValue::Undefined),
1165 other => {
1166 single_holder = [other.clone()];
1167 &single_holder[..]
1168 }
1169 };
1170 let (start_idx, mut accumulator) = if let Some(init_expr) = initial {
1171 let init_val = eval_compiled_inner(init_expr, data, vars, ctx, shape)?;
1172 if items.is_empty() {
1173 return Ok(init_val);
1174 }
1175 (0usize, init_val)
1176 } else {
1177 if items.is_empty() {
1178 return Ok(JValue::Null);
1179 }
1180 (1, items[0].clone())
1181 };
1182 let acc_param = params[0].as_str();
1183 let item_param = params[1].as_str();
1184 for item in items[start_idx..].iter() {
1185 // Per-iteration HashMap: &accumulator borrow must be released before we
1186 // can reassign `accumulator`. `drop(call_vars)` ends the borrow.
1187 let mut call_vars = clone_outer_vars(vars, 2);
1188 call_vars.insert(acc_param, &accumulator);
1189 call_vars.insert(item_param, item);
1190 let new_acc = eval_compiled_inner(body, data, Some(&call_vars), ctx, shape)?;
1191 drop(call_vars);
1192 accumulator = new_acc;
1193 }
1194 Ok(accumulator)
1195 }
1196 }
1197}
1198
1199/// Truthiness check (matches JSONata semantics). Standalone function for compiled path.
1200#[inline]
1201pub(crate) fn compiled_is_truthy(value: &JValue) -> bool {
1202 match value {
1203 JValue::Null | JValue::Undefined => false,
1204 JValue::Bool(b) => *b,
1205 JValue::Number(n) => *n != 0.0,
1206 JValue::String(s) => !s.is_empty(),
1207 JValue::Array(a) => !a.is_empty(),
1208 JValue::Object(o) => !o.is_empty(),
1209 _ => false,
1210 }
1211}
1212
1213/// Returns true if the compiled expression is a literal `null` (from `AstNode::Null`).
1214/// Used to replicate the tree-walker's `explicit_null` flag in comparisons/arithmetic.
1215#[inline]
1216fn is_compiled_explicit_null(expr: &CompiledExpr) -> bool {
1217 matches!(expr, CompiledExpr::ExplicitNull)
1218}
1219
1220/// Ordered comparison for compiled expressions.
1221/// Mirrors the tree-walker's `ordered_compare` including explicit-null semantics.
1222#[inline]
1223pub(crate) fn compiled_ordered_cmp(
1224 left: &JValue,
1225 right: &JValue,
1226 left_is_explicit_null: bool,
1227 right_is_explicit_null: bool,
1228 cmp_num: fn(f64, f64) -> bool,
1229 cmp_str: fn(&str, &str) -> bool,
1230) -> Result<JValue, EvaluatorError> {
1231 match (left, right) {
1232 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Bool(cmp_num(*a, *b))),
1233 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(cmp_str(a, b))),
1234 // Both null/undefined → undefined
1235 (JValue::Null, JValue::Null) | (JValue::Undefined, JValue::Undefined) => Ok(JValue::Null),
1236 (JValue::Undefined, JValue::Null) | (JValue::Null, JValue::Undefined) => Ok(JValue::Null),
1237 // Explicit null literal with any non-null type → T2010 error
1238 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::EvaluationError(
1239 "T2010: Type mismatch in comparison".to_string(),
1240 )),
1241 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::EvaluationError(
1242 "T2010: Type mismatch in comparison".to_string(),
1243 )),
1244 // Boolean with undefined → T2010 error
1245 (JValue::Bool(_), JValue::Null | JValue::Undefined)
1246 | (JValue::Null | JValue::Undefined, JValue::Bool(_)) => Err(
1247 EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()),
1248 ),
1249 // Number or String with implicit undefined (missing field) → undefined result
1250 (JValue::Number(_) | JValue::String(_), JValue::Null | JValue::Undefined)
1251 | (JValue::Null | JValue::Undefined, JValue::Number(_) | JValue::String(_)) => {
1252 Ok(JValue::Null)
1253 }
1254 // Type mismatch (string vs number)
1255 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
1256 Err(EvaluatorError::EvaluationError(
1257 "T2009: The expressions on either side of operator must be of the same data type"
1258 .to_string(),
1259 ))
1260 }
1261 _ => Err(EvaluatorError::EvaluationError(
1262 "T2010: Type mismatch in comparison".to_string(),
1263 )),
1264 }
1265}
1266
1267/// Arithmetic for compiled expressions.
1268/// Mirrors the tree-walker's arithmetic functions including explicit-null semantics.
1269#[inline]
1270pub(crate) fn compiled_arithmetic(
1271 op: CompiledArithOp,
1272 left: &JValue,
1273 right: &JValue,
1274 left_is_explicit_null: bool,
1275 right_is_explicit_null: bool,
1276) -> Result<JValue, EvaluatorError> {
1277 let op_sym = match op {
1278 CompiledArithOp::Add => "+",
1279 CompiledArithOp::Sub => "-",
1280 CompiledArithOp::Mul => "*",
1281 CompiledArithOp::Div => "/",
1282 CompiledArithOp::Mod => "%",
1283 };
1284 match (left, right) {
1285 (JValue::Number(a), JValue::Number(b)) => {
1286 let result = match op {
1287 CompiledArithOp::Add => *a + *b,
1288 CompiledArithOp::Sub => *a - *b,
1289 CompiledArithOp::Mul => {
1290 let r = *a * *b;
1291 if r.is_infinite() {
1292 return Err(EvaluatorError::EvaluationError(
1293 "D1001: Number out of range".to_string(),
1294 ));
1295 }
1296 r
1297 }
1298 CompiledArithOp::Div => {
1299 if *b == 0.0 {
1300 return Err(EvaluatorError::EvaluationError(
1301 "Division by zero".to_string(),
1302 ));
1303 }
1304 *a / *b
1305 }
1306 CompiledArithOp::Mod => {
1307 if *b == 0.0 {
1308 return Err(EvaluatorError::EvaluationError(
1309 "Division by zero".to_string(),
1310 ));
1311 }
1312 *a % *b
1313 }
1314 };
1315 Ok(JValue::Number(result))
1316 }
1317 // Explicit null literal → T2002 error (matching tree-walker behavior)
1318 (JValue::Null | JValue::Undefined, _) if left_is_explicit_null => {
1319 Err(EvaluatorError::TypeError(format!(
1320 "T2002: The left side of the {} operator must evaluate to a number",
1321 op_sym
1322 )))
1323 }
1324 (_, JValue::Null | JValue::Undefined) if right_is_explicit_null => {
1325 Err(EvaluatorError::TypeError(format!(
1326 "T2002: The right side of the {} operator must evaluate to a number",
1327 op_sym
1328 )))
1329 }
1330 // Implicit undefined propagation (from missing field) → undefined result
1331 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
1332 Ok(JValue::Null)
1333 }
1334 _ => Err(EvaluatorError::TypeError(format!(
1335 "Cannot apply {} to {:?} and {:?}",
1336 op_sym, left, right
1337 ))),
1338 }
1339}
1340
1341/// Convert a value to string for concatenation in compiled expressions.
1342#[inline]
1343pub(crate) fn compiled_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
1344 match value {
1345 JValue::String(s) => Ok(s.to_string()),
1346 JValue::Null | JValue::Undefined => Ok(String::new()),
1347 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
1348 match crate::functions::string::string(value, None) {
1349 Ok(JValue::String(s)) => Ok(s.to_string()),
1350 Ok(JValue::Null) => Ok(String::new()),
1351 _ => Err(EvaluatorError::TypeError(
1352 "Cannot concatenate complex types".to_string(),
1353 )),
1354 }
1355 }
1356 _ => Ok(String::new()),
1357 }
1358}
1359
1360/// Equality comparison for the bytecode VM.
1361#[inline]
1362pub(crate) fn compiled_equal(lhs: &JValue, rhs: &JValue) -> JValue {
1363 JValue::Bool(crate::functions::array::values_equal(lhs, rhs))
1364}
1365
1366/// String concatenation for the bytecode VM.
1367#[inline]
1368pub(crate) fn compiled_concat(lhs: JValue, rhs: JValue) -> Result<JValue, EvaluatorError> {
1369 let l = compiled_to_concat_string(&lhs)?;
1370 let r = compiled_to_concat_string(&rhs)?;
1371 Ok(JValue::string(l + &r))
1372}
1373
1374/// Entry point for the bytecode VM to call pure builtins by name.
1375#[inline]
1376pub(crate) fn call_pure_builtin_by_name(
1377 name: &str,
1378 args: &[JValue],
1379 data: &JValue,
1380) -> Result<JValue, EvaluatorError> {
1381 call_pure_builtin(name, args, data)
1382}
1383
1384// ──────────────────────────────────────────────────────────────────────────────
1385// Phase 2: path compilation, builtin dispatch, and supporting helpers
1386// ──────────────────────────────────────────────────────────────────────────────
1387
1388/// Compile a `Path { steps }` AstNode into a `CompiledExpr`.
1389///
1390/// Handles paths like `a.b.c`, `a[pred].b`, `$var.field`.
1391/// Returns `None` if any step is not compilable (e.g. wildcards, function apps).
1392fn try_compile_path(
1393 steps: &[crate::ast::PathStep],
1394 allowed_vars: Option<&[&str]>,
1395) -> Option<CompiledExpr> {
1396 use crate::ast::{AstNode, Stage};
1397
1398 if steps.is_empty() {
1399 return None;
1400 }
1401
1402 // Determine the start of the path:
1403 // `$.field...` → starts from current data (drop the leading `$` step)
1404 // `$var.field` → variable-prefixed paths: not compiled yet, fall back to tree-walker
1405 // `field...` → starts from current data
1406 let field_steps: &[crate::ast::PathStep] = match &steps[0].node {
1407 AstNode::Variable(var) if var.is_empty() && steps[0].stages.is_empty() => &steps[1..],
1408 AstNode::Variable(_) => return None,
1409 AstNode::Name(_) => steps,
1410 _ => return None,
1411 };
1412
1413 // Compile a boolean filter predicate, rejecting numeric predicates (`[0]`, `[1]`)
1414 // which represent index access in JSONata, not boolean filtering, and the
1415 // explicit `[]` keep-array marker (`Boolean(true)`), which forces the result
1416 // to stay an array rather than filtering — the tree-walker's
1417 // `evaluate_predicate` special-cases it and the compiled path has no
1418 // equivalent, so bail out rather than silently treating it as `filter(true)`.
1419 let compile_filter = |node: &AstNode| -> Option<CompiledExpr> {
1420 if matches!(node, AstNode::Number(_) | AstNode::Boolean(true)) {
1421 return None;
1422 }
1423 try_compile_expr_inner(node, allowed_vars)
1424 };
1425
1426 // Compile each field step.
1427 // Handles:
1428 // - Name nodes with at most one Stage::Filter attached (from `a.b[pred]` dot-path parsing)
1429 // - Predicate nodes (from `products[pred]` standalone predicate parsing) — folded into the
1430 // previous step's filter slot, since both encodings have identical runtime semantics.
1431 let mut compiled_steps = Vec::with_capacity(field_steps.len());
1432 for step in field_steps {
1433 // Tuple-stream steps (@ focus / # index / % parent binding) require the
1434 // tree-walker's tuple machinery (create_tuple_stream / evaluate_path's
1435 // tuple handling). Never compile them to the flat bytecode field path,
1436 // which is unaware of the binding flags and would silently drop them.
1437 if step.focus.is_some()
1438 || step.index_var.is_some()
1439 || step.ancestor_label.is_some()
1440 || step.is_tuple
1441 {
1442 return None;
1443 }
1444 match &step.node {
1445 AstNode::Name(name) => {
1446 let filter = match step.stages.as_slice() {
1447 [] => None,
1448 [Stage::Filter(filter_node)] => Some(compile_filter(filter_node)?),
1449 _ => return None,
1450 };
1451 compiled_steps.push(CompiledStep {
1452 field: name.clone(),
1453 filter,
1454 });
1455 }
1456 AstNode::Predicate(filter_node) => {
1457 // Standalone predicate step — fold into the previous Name step's filter slot.
1458 if !step.stages.is_empty() {
1459 return None;
1460 }
1461 let last = compiled_steps.last_mut()?;
1462 if last.filter.is_some() {
1463 return None;
1464 }
1465 last.filter = Some(compile_filter(filter_node)?);
1466 }
1467 _ => return None,
1468 }
1469 }
1470
1471 if compiled_steps.is_empty() {
1472 // Bare `$` with no further field steps — current-data reference
1473 return Some(CompiledExpr::VariableLookup(String::new()));
1474 }
1475
1476 // Shape-cache optimizations (FieldLookup / NestedFieldLookup) are only safe
1477 // in HOF mode (allowed_vars=Some), where data is always a single Object element
1478 // from an array. In top-level mode (allowed_vars=None), data can itself be an
1479 // Array, so we must use FieldPath which applies implicit array-mapping semantics.
1480 if allowed_vars.is_some() {
1481 if compiled_steps.len() == 1 && compiled_steps[0].filter.is_none() {
1482 return Some(CompiledExpr::FieldLookup(compiled_steps.remove(0).field));
1483 }
1484 if compiled_steps.len() == 2
1485 && compiled_steps[0].filter.is_none()
1486 && compiled_steps[1].filter.is_none()
1487 {
1488 let outer = compiled_steps.remove(0).field;
1489 let inner = compiled_steps.remove(0).field;
1490 return Some(CompiledExpr::NestedFieldLookup(outer, inner));
1491 }
1492 }
1493
1494 Some(CompiledExpr::FieldPath(compiled_steps))
1495}
1496
1497/// Evaluate a compiled `FieldPath` against `data`.
1498///
1499/// Applies implicit array-mapping semantics at each step (matching the tree-walker).
1500/// Filters are applied as predicates: truthy elements are kept.
1501///
1502/// Singleton unwrapping mirrors the tree-walker's `did_array_mapping` rule:
1503/// - Extracting a field from an *array* sets the mapping flag (unwrap singletons at end).
1504/// - Extracting a field from a *single object* resets the flag (preserve the raw value).
1505fn compiled_eval_field_path(
1506 steps: &[CompiledStep],
1507 data: &JValue,
1508 vars: Option<&HashMap<&str, &JValue>>,
1509 ctx: Option<&Context>,
1510 shape: Option<&ShapeCache>,
1511) -> Result<JValue, EvaluatorError> {
1512 let mut current = data.clone();
1513 // Track whether the most recent field step mapped over an array (like the tree-walker's
1514 // `did_array_mapping` flag). Filters also count as array operations.
1515 let mut did_array_mapping = false;
1516 for step in steps {
1517 // Determine if this step will do array mapping before we overwrite `current`
1518 let is_array = matches!(current, JValue::Array(_));
1519 // Field access with implicit array mapping
1520 current = compiled_field_step(&step.field, ¤t);
1521 if is_array {
1522 did_array_mapping = true;
1523 } else {
1524 // Extracting from a single object resets the flag (tree-walker parity)
1525 did_array_mapping = false;
1526 }
1527 // Apply filter if present (filter is an array operation — keep the flag set)
1528 if let Some(filter) = &step.filter {
1529 current = compiled_apply_filter(filter, ¤t, vars, ctx, shape)?;
1530 // Filter always implies we operated on an array
1531 did_array_mapping = true;
1532 }
1533 }
1534 // Singleton unwrapping: only when array-mapping occurred, matching tree-walker.
1535 if did_array_mapping {
1536 Ok(match current {
1537 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
1538 other => other,
1539 })
1540 } else {
1541 Ok(current)
1542 }
1543}
1544
1545/// Perform a single-field access with implicit array-mapping semantics.
1546///
1547/// - Object: look up `field`, return its value or Undefined
1548/// - Array: map field extraction over each element, flatten nested arrays, skip Undefined
1549/// - Tuple objects (`__tuple__: true`): look up in the `@` inner object
1550/// - Other: Undefined
1551fn compiled_field_step(field: &str, value: &JValue) -> JValue {
1552 match value {
1553 JValue::Object(obj) => {
1554 // Check for tuple: extract from "@" inner object
1555 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1556 if let Some(JValue::Object(inner)) = obj.get("@") {
1557 return inner.get(field).cloned().unwrap_or(JValue::Undefined);
1558 }
1559 return JValue::Undefined;
1560 }
1561 obj.get(field).cloned().unwrap_or(JValue::Undefined)
1562 }
1563 JValue::Array(arr) => {
1564 // Build shape cache from first plain (non-tuple) object for O(1) positional access.
1565 let shape: Option<ShapeCache> = arr.iter().find_map(|v| {
1566 if let JValue::Object(obj) = v {
1567 if obj.get("__tuple__") != Some(&JValue::Bool(true)) {
1568 return build_shape_cache(v);
1569 }
1570 }
1571 None
1572 });
1573 let mut result = Vec::new();
1574 for item in arr.iter() {
1575 let extracted = if let (Some(ref sh), JValue::Object(obj)) = (&shape, item) {
1576 // Tuple objects need the recursive path for "@" inner lookup.
1577 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
1578 compiled_field_step(field, item)
1579 } else if let Some(&pos) = sh.get(field) {
1580 // Positional access with key verification: guards against heterogeneous
1581 // schemas (objects where the same field is at a different index).
1582 // On a mismatch, fall back to a regular hash lookup.
1583 match obj.get_index(pos) {
1584 Some((k, v)) if k.as_str() == field => v.clone(),
1585 _ => obj.get(field).cloned().unwrap_or(JValue::Undefined),
1586 }
1587 } else {
1588 // Field not in the first object's schema — fall back to hash lookup
1589 // so that heterogeneous arrays (e.g. [{a:1},{b:2}]) are handled correctly.
1590 obj.get(field).cloned().unwrap_or(JValue::Undefined)
1591 }
1592 } else {
1593 compiled_field_step(field, item)
1594 };
1595 match extracted {
1596 JValue::Undefined => {}
1597 JValue::Array(inner) => result.extend(inner.iter().cloned()),
1598 other => result.push(other),
1599 }
1600 }
1601 if result.is_empty() {
1602 JValue::Undefined
1603 } else {
1604 JValue::array(result)
1605 }
1606 }
1607 _ => JValue::Undefined,
1608 }
1609}
1610
1611/// Apply a compiled filter predicate to a value.
1612///
1613/// - Array: return elements for which the predicate is truthy
1614/// - Single value: return it if predicate is truthy, else Undefined
1615/// - Numeric predicates (index access) are NOT supported here — fall back via None compilation
1616fn compiled_apply_filter(
1617 filter: &CompiledExpr,
1618 value: &JValue,
1619 vars: Option<&HashMap<&str, &JValue>>,
1620 ctx: Option<&Context>,
1621 shape: Option<&ShapeCache>,
1622) -> Result<JValue, EvaluatorError> {
1623 match value {
1624 JValue::Array(arr) => {
1625 let mut result = Vec::new();
1626 // Auto-build shape cache from first element when not provided.
1627 // Avoids per-element hash lookups in the filter predicate for homogeneous arrays.
1628 let local_shape: Option<ShapeCache> = if shape.is_none() {
1629 arr.first().and_then(build_shape_cache)
1630 } else {
1631 None
1632 };
1633 let effective_shape = shape.or(local_shape.as_ref());
1634 for item in arr.iter() {
1635 let pred = eval_compiled_inner(filter, item, vars, ctx, effective_shape)?;
1636 if compiled_is_truthy(&pred) {
1637 result.push(item.clone());
1638 }
1639 }
1640 if result.is_empty() {
1641 Ok(JValue::Undefined)
1642 } else if result.len() == 1 {
1643 Ok(result.remove(0))
1644 } else {
1645 Ok(JValue::array(result))
1646 }
1647 }
1648 JValue::Undefined => Ok(JValue::Undefined),
1649 _ => {
1650 let pred = eval_compiled_inner(filter, value, vars, ctx, shape)?;
1651 if compiled_is_truthy(&pred) {
1652 Ok(value.clone())
1653 } else {
1654 Ok(JValue::Undefined)
1655 }
1656 }
1657 }
1658}
1659
1660/// Dispatch a pure builtin function call.
1661///
1662/// Replicates the tree-walker's evaluation for the subset of builtins in
1663/// `COMPILABLE_BUILTINS`: no side effects, no lambdas, no context mutations.
1664/// `data` is the current context value for implicit-argument insertion.
1665fn call_pure_builtin(name: &str, args: &[JValue], data: &JValue) -> Result<JValue, EvaluatorError> {
1666 use crate::functions;
1667
1668 // Apply implicit context insertion matching the tree-walker
1669 let args_storage: Vec<JValue>;
1670 let effective_args: &[JValue] = if args.is_empty() {
1671 match name {
1672 "string" => {
1673 // $string() with a null/undefined context returns undefined, not "null".
1674 // This mirrors the tree-walker's special case at the function-call site.
1675 if data.is_undefined() || data.is_null() {
1676 return Ok(JValue::Undefined);
1677 }
1678 args_storage = vec![data.clone()];
1679 &args_storage
1680 }
1681 "number" | "boolean" | "uppercase" | "lowercase" => {
1682 args_storage = vec![data.clone()];
1683 &args_storage
1684 }
1685 _ => args,
1686 }
1687 } else if args.len() == 1 {
1688 match name {
1689 "substringBefore" | "substringAfter" | "contains" | "split" => {
1690 if matches!(data, JValue::String(_)) {
1691 args_storage = std::iter::once(data.clone())
1692 .chain(args.iter().cloned())
1693 .collect();
1694 &args_storage
1695 } else {
1696 args
1697 }
1698 }
1699 _ => args,
1700 }
1701 } else {
1702 args
1703 };
1704
1705 // Apply undefined propagation: if the first effective argument is Undefined
1706 // and the function propagates undefined, return Undefined immediately.
1707 // This matches the tree-walker's `propagates_undefined` check.
1708 if effective_args.first().is_some_and(JValue::is_undefined) && propagates_undefined(name) {
1709 return Ok(JValue::Undefined);
1710 }
1711
1712 match name {
1713 // ── String functions ────────────────────────────────────────────
1714 "string" => {
1715 // Validate the optional prettify argument: must be a boolean.
1716 let prettify = match effective_args.get(1) {
1717 None => None,
1718 Some(JValue::Bool(b)) => Some(*b),
1719 Some(_) => {
1720 return Err(EvaluatorError::TypeError(
1721 "string() prettify parameter must be a boolean".to_string(),
1722 ))
1723 }
1724 };
1725 let arg = effective_args.first().unwrap_or(&JValue::Null);
1726 Ok(functions::string::string(arg, prettify)?)
1727 }
1728 "length" => match effective_args.first() {
1729 Some(JValue::String(s)) => Ok(functions::string::length(s)?),
1730 // Undefined input propagates (caught above by the undefined-propagation guard).
1731 Some(JValue::Undefined) => Ok(JValue::Undefined),
1732 // No argument: mirrors tree-walker "requires exactly 1 argument" (no error code,
1733 // so the test framework accepts it against any expected T-code).
1734 None => Err(EvaluatorError::EvaluationError(
1735 "length() requires exactly 1 argument".to_string(),
1736 )),
1737 // null and any other non-string type → T0410
1738 _ => Err(EvaluatorError::TypeError(
1739 "T0410: Argument 1 of function length does not match function signature"
1740 .to_string(),
1741 )),
1742 },
1743 "uppercase" => match effective_args.first() {
1744 Some(JValue::String(s)) => Ok(functions::string::uppercase(s)?),
1745 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1746 _ => Err(EvaluatorError::TypeError(
1747 "T0410: Argument 1 of function uppercase does not match function signature"
1748 .to_string(),
1749 )),
1750 },
1751 "lowercase" => match effective_args.first() {
1752 Some(JValue::String(s)) => Ok(functions::string::lowercase(s)?),
1753 Some(JValue::Undefined) | None => Ok(JValue::Undefined),
1754 _ => Err(EvaluatorError::TypeError(
1755 "T0410: Argument 1 of function lowercase does not match function signature"
1756 .to_string(),
1757 )),
1758 },
1759 "trim" => match effective_args.first() {
1760 None | Some(JValue::Null | JValue::Undefined) => Ok(JValue::Null),
1761 Some(JValue::String(s)) => Ok(functions::string::trim(s)?),
1762 _ => Err(EvaluatorError::TypeError(
1763 "trim() requires a string argument".to_string(),
1764 )),
1765 },
1766 "substring" => {
1767 if effective_args.len() < 2 {
1768 return Err(EvaluatorError::EvaluationError(
1769 "substring() requires at least 2 arguments".to_string(),
1770 ));
1771 }
1772 match (&effective_args[0], &effective_args[1]) {
1773 (JValue::String(s), JValue::Number(start)) => {
1774 // Optional 3rd arg (length) must be a number if provided.
1775 let length = match effective_args.get(2) {
1776 None => None,
1777 Some(JValue::Number(l)) => Some(*l as i64),
1778 Some(_) => {
1779 return Err(EvaluatorError::TypeError(
1780 "T0410: Argument 3 of function substring does not match function signature"
1781 .to_string(),
1782 ))
1783 }
1784 };
1785 Ok(functions::string::substring(s, *start as i64, length)?)
1786 }
1787 _ => Err(EvaluatorError::TypeError(
1788 "T0410: Argument 1 of function substring does not match function signature"
1789 .to_string(),
1790 )),
1791 }
1792 }
1793 "substringBefore" => {
1794 if effective_args.len() != 2 {
1795 return Err(EvaluatorError::TypeError(
1796 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
1797 ));
1798 }
1799 match (&effective_args[0], &effective_args[1]) {
1800 (JValue::String(s), JValue::String(sep)) => {
1801 Ok(functions::string::substring_before(s, sep)?)
1802 }
1803 // Undefined propagates; null is a type error.
1804 (JValue::Undefined, _) => Ok(JValue::Undefined),
1805 _ => Err(EvaluatorError::TypeError(
1806 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
1807 )),
1808 }
1809 }
1810 "substringAfter" => {
1811 if effective_args.len() != 2 {
1812 return Err(EvaluatorError::TypeError(
1813 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
1814 ));
1815 }
1816 match (&effective_args[0], &effective_args[1]) {
1817 (JValue::String(s), JValue::String(sep)) => {
1818 Ok(functions::string::substring_after(s, sep)?)
1819 }
1820 // Undefined propagates; null is a type error.
1821 (JValue::Undefined, _) => Ok(JValue::Undefined),
1822 _ => Err(EvaluatorError::TypeError(
1823 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
1824 )),
1825 }
1826 }
1827 "contains" => {
1828 if effective_args.len() != 2 {
1829 return Err(EvaluatorError::EvaluationError(
1830 "contains() requires exactly 2 arguments".to_string(),
1831 ));
1832 }
1833 match &effective_args[0] {
1834 JValue::Null | JValue::Undefined => Ok(JValue::Null),
1835 JValue::String(s) => Ok(functions::string::contains(s, &effective_args[1])?),
1836 _ => Err(EvaluatorError::TypeError(
1837 "contains() requires a string as the first argument".to_string(),
1838 )),
1839 }
1840 }
1841 "split" => {
1842 if effective_args.len() < 2 {
1843 return Err(EvaluatorError::EvaluationError(
1844 "split() requires at least 2 arguments".to_string(),
1845 ));
1846 }
1847 match &effective_args[0] {
1848 JValue::Null | JValue::Undefined => Ok(JValue::Null),
1849 JValue::String(s) => {
1850 // Validate the optional limit argument — must be a positive number.
1851 let limit = match effective_args.get(2) {
1852 None => None,
1853 Some(JValue::Number(n)) => {
1854 if *n < 0.0 {
1855 return Err(EvaluatorError::EvaluationError(
1856 "D3020: Third argument of split function must be a positive number"
1857 .to_string(),
1858 ));
1859 }
1860 Some(n.floor() as usize)
1861 }
1862 Some(_) => {
1863 return Err(EvaluatorError::TypeError(
1864 "split() limit must be a number".to_string(),
1865 ))
1866 }
1867 };
1868 Ok(functions::string::split(s, &effective_args[1], limit)?)
1869 }
1870 _ => Err(EvaluatorError::TypeError(
1871 "split() requires a string as the first argument".to_string(),
1872 )),
1873 }
1874 }
1875 "join" => {
1876 if effective_args.is_empty() {
1877 return Err(EvaluatorError::TypeError(
1878 "T0410: Argument 1 of function $join does not match function signature"
1879 .to_string(),
1880 ));
1881 }
1882 match &effective_args[0] {
1883 JValue::Null | JValue::Undefined => Ok(JValue::Null),
1884 // Signature: <a<s>s?:s> — first arg must be an array of strings.
1885 JValue::Bool(_) | JValue::Number(_) | JValue::Object(_) => {
1886 Err(EvaluatorError::TypeError(
1887 "T0412: Argument 1 of function $join must be an array of String"
1888 .to_string(),
1889 ))
1890 }
1891 JValue::Array(arr) => {
1892 // All elements must be strings.
1893 for item in arr.iter() {
1894 if !matches!(item, JValue::String(_)) {
1895 return Err(EvaluatorError::TypeError(
1896 "T0412: Argument 1 of function $join must be an array of String"
1897 .to_string(),
1898 ));
1899 }
1900 }
1901 // Validate separator: must be a string if provided.
1902 let separator = match effective_args.get(1) {
1903 None | Some(JValue::Undefined) => None,
1904 Some(JValue::String(s)) => Some(&**s),
1905 Some(_) => {
1906 return Err(EvaluatorError::TypeError(
1907 "T0410: Argument 2 of function $join does not match function signature (expected String)"
1908 .to_string(),
1909 ))
1910 }
1911 };
1912 Ok(functions::string::join(arr, separator)?)
1913 }
1914 JValue::String(s) => Ok(JValue::String(s.clone())),
1915 _ => Err(EvaluatorError::TypeError(
1916 "T0412: Argument 1 of function $join must be an array of String".to_string(),
1917 )),
1918 }
1919 }
1920
1921 // ── Numeric functions ───────────────────────────────────────────
1922 "number" => match effective_args.first() {
1923 Some(v) => Ok(functions::numeric::number(v)?),
1924 None => Err(EvaluatorError::EvaluationError(
1925 "number() requires at least 1 argument".to_string(),
1926 )),
1927 },
1928 "floor" => match effective_args.first() {
1929 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1930 Some(JValue::Number(n)) => Ok(functions::numeric::floor(*n)?),
1931 _ => Err(EvaluatorError::TypeError(
1932 "floor() requires a number argument".to_string(),
1933 )),
1934 },
1935 "ceil" => match effective_args.first() {
1936 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1937 Some(JValue::Number(n)) => Ok(functions::numeric::ceil(*n)?),
1938 _ => Err(EvaluatorError::TypeError(
1939 "ceil() requires a number argument".to_string(),
1940 )),
1941 },
1942 "round" => match effective_args.first() {
1943 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1944 Some(JValue::Number(n)) => {
1945 let precision = effective_args.get(1).and_then(|v| {
1946 if let JValue::Number(p) = v {
1947 Some(*p as i32)
1948 } else {
1949 None
1950 }
1951 });
1952 Ok(functions::numeric::round(*n, precision)?)
1953 }
1954 _ => Err(EvaluatorError::TypeError(
1955 "round() requires a number argument".to_string(),
1956 )),
1957 },
1958 "abs" => match effective_args.first() {
1959 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1960 Some(JValue::Number(n)) => Ok(functions::numeric::abs(*n)?),
1961 _ => Err(EvaluatorError::TypeError(
1962 "abs() requires a number argument".to_string(),
1963 )),
1964 },
1965 "sqrt" => match effective_args.first() {
1966 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
1967 Some(JValue::Number(n)) => Ok(functions::numeric::sqrt(*n)?),
1968 _ => Err(EvaluatorError::TypeError(
1969 "sqrt() requires a number argument".to_string(),
1970 )),
1971 },
1972
1973 // ── Aggregation functions ───────────────────────────────────────
1974 "sum" => match effective_args.first() {
1975 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1976 None => Err(EvaluatorError::EvaluationError(
1977 "sum() requires exactly 1 argument".to_string(),
1978 )),
1979 Some(JValue::Null) => Ok(JValue::Null),
1980 Some(JValue::Array(arr)) => Ok(aggregation::sum(arr)?),
1981 Some(JValue::Number(n)) => Ok(JValue::Number(*n)),
1982 Some(other) => Ok(functions::numeric::sum(&[other.clone()])?),
1983 },
1984 "max" => match effective_args.first() {
1985 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1986 Some(JValue::Null) | None => Ok(JValue::Null),
1987 Some(JValue::Array(arr)) => Ok(aggregation::max(arr)?),
1988 Some(v @ JValue::Number(_)) => Ok(v.clone()),
1989 _ => Err(EvaluatorError::TypeError(
1990 "max() requires an array or number argument".to_string(),
1991 )),
1992 },
1993 "min" => match effective_args.first() {
1994 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
1995 Some(JValue::Null) | None => Ok(JValue::Null),
1996 Some(JValue::Array(arr)) => Ok(aggregation::min(arr)?),
1997 Some(v @ JValue::Number(_)) => Ok(v.clone()),
1998 _ => Err(EvaluatorError::TypeError(
1999 "min() requires an array or number argument".to_string(),
2000 )),
2001 },
2002 "average" => match effective_args.first() {
2003 Some(v) if v.is_undefined() => Ok(JValue::Undefined),
2004 Some(JValue::Null) | None => Ok(JValue::Null),
2005 Some(JValue::Array(arr)) => Ok(aggregation::average(arr)?),
2006 Some(v @ JValue::Number(_)) => Ok(v.clone()),
2007 _ => Err(EvaluatorError::TypeError(
2008 "average() requires an array or number argument".to_string(),
2009 )),
2010 },
2011 "count" => match effective_args.first() {
2012 Some(v) if v.is_undefined() => Ok(JValue::from(0i64)),
2013 Some(JValue::Null) | None => Ok(JValue::from(0i64)),
2014 Some(JValue::Array(arr)) => Ok(functions::array::count(arr)?),
2015 _ => Ok(JValue::from(1i64)),
2016 },
2017
2018 // ── Boolean / logic ─────────────────────────────────────────────
2019 "boolean" => match effective_args.first() {
2020 Some(v) => Ok(functions::boolean::boolean(v)?),
2021 None => Err(EvaluatorError::EvaluationError(
2022 "boolean() requires 1 argument".to_string(),
2023 )),
2024 },
2025 "not" => match effective_args.first() {
2026 Some(v) => Ok(JValue::Bool(!compiled_is_truthy(v))),
2027 None => Err(EvaluatorError::EvaluationError(
2028 "not() requires 1 argument".to_string(),
2029 )),
2030 },
2031
2032 // ── Array functions ─────────────────────────────────────────────
2033 "append" => {
2034 if effective_args.len() != 2 {
2035 return Err(EvaluatorError::EvaluationError(
2036 "append() requires exactly 2 arguments".to_string(),
2037 ));
2038 }
2039 let first = &effective_args[0];
2040 let second = &effective_args[1];
2041 if matches!(second, JValue::Null | JValue::Undefined) {
2042 return Ok(first.clone());
2043 }
2044 if matches!(first, JValue::Null | JValue::Undefined) {
2045 return Ok(second.clone());
2046 }
2047 let arr = match first {
2048 JValue::Array(a) => a.to_vec(),
2049 other => vec![other.clone()],
2050 };
2051 Ok(functions::array::append(&arr, second)?)
2052 }
2053 "reverse" => match effective_args.first() {
2054 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2055 Some(JValue::Array(arr)) => Ok(functions::array::reverse(arr)?),
2056 _ => Err(EvaluatorError::TypeError(
2057 "reverse() requires an array argument".to_string(),
2058 )),
2059 },
2060 "distinct" => match effective_args.first() {
2061 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2062 Some(JValue::Array(arr)) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
2063 // Non-array input, and arrays of length <= 1, pass through unchanged
2064 // (jsonata-js functions.js: `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
2065 Some(other) => Ok(other.clone()),
2066 },
2067
2068 // ── Object functions ────────────────────────────────────────────
2069 "keys" => match effective_args.first() {
2070 Some(JValue::Null | JValue::Undefined) | None => Ok(JValue::Null),
2071 Some(JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(JValue::Null),
2072 Some(JValue::Object(obj)) => {
2073 if obj.is_empty() {
2074 Ok(JValue::Null)
2075 } else {
2076 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
2077 if keys.len() == 1 {
2078 Ok(keys.into_iter().next().unwrap())
2079 } else {
2080 Ok(JValue::array(keys))
2081 }
2082 }
2083 }
2084 Some(JValue::Array(arr)) => {
2085 let mut all_keys: Vec<JValue> = Vec::new();
2086 for item in arr.iter() {
2087 if let JValue::Object(obj) = item {
2088 for key in obj.keys() {
2089 let k = JValue::string(key.clone());
2090 if !all_keys.contains(&k) {
2091 all_keys.push(k);
2092 }
2093 }
2094 }
2095 }
2096 if all_keys.is_empty() {
2097 Ok(JValue::Null)
2098 } else if all_keys.len() == 1 {
2099 Ok(all_keys.into_iter().next().unwrap())
2100 } else {
2101 Ok(JValue::array(all_keys))
2102 }
2103 }
2104 _ => Ok(JValue::Null),
2105 },
2106 "merge" => match effective_args.len() {
2107 0 => Err(EvaluatorError::EvaluationError(
2108 "merge() requires at least 1 argument".to_string(),
2109 )),
2110 1 => match &effective_args[0] {
2111 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
2112 JValue::Null | JValue::Undefined => Ok(JValue::Null),
2113 JValue::Object(_) => Ok(effective_args[0].clone()),
2114 _ => Err(EvaluatorError::TypeError(
2115 "merge() requires objects or an array of objects".to_string(),
2116 )),
2117 },
2118 _ => Ok(functions::object::merge(effective_args)?),
2119 },
2120
2121 _ => unreachable!(
2122 "call_pure_builtin called with non-compilable builtin: {}",
2123 name
2124 ),
2125 }
2126}
2127
2128// ──────────────────────────────────────────────────────────────────────────────
2129// End of CompiledExpr framework
2130// ──────────────────────────────────────────────────────────────────────────────
2131
2132/// Functions that propagate undefined (return undefined when given an undefined argument).
2133/// These functions should return null/undefined when their input path doesn't exist,
2134/// rather than throwing a type error.
2135const UNDEFINED_PROPAGATING_FUNCTIONS: &[&str] = &[
2136 "not",
2137 "boolean",
2138 "length",
2139 "number",
2140 "uppercase",
2141 "lowercase",
2142 "substring",
2143 "substringBefore",
2144 "substringAfter",
2145 "string",
2146];
2147
2148/// Check whether a function propagates undefined values
2149fn propagates_undefined(name: &str) -> bool {
2150 UNDEFINED_PROPAGATING_FUNCTIONS.contains(&name)
2151}
2152
2153/// Iterator-based numeric aggregation helpers.
2154/// These avoid cloning values by iterating over references and extracting f64 values directly.
2155mod aggregation {
2156 use super::*;
2157
2158 /// Iterate over all numeric values in a potentially nested array, yielding f64 values.
2159 /// Returns Err if any non-numeric value is encountered.
2160 fn for_each_numeric(
2161 arr: &[JValue],
2162 func_name: &str,
2163 mut f: impl FnMut(f64),
2164 ) -> Result<(), EvaluatorError> {
2165 fn recurse(
2166 arr: &[JValue],
2167 func_name: &str,
2168 f: &mut dyn FnMut(f64),
2169 ) -> Result<(), EvaluatorError> {
2170 for value in arr.iter() {
2171 match value {
2172 JValue::Array(inner) => recurse(inner, func_name, f)?,
2173 JValue::Number(n) => {
2174 f(*n);
2175 }
2176 _ => {
2177 return Err(EvaluatorError::TypeError(format!(
2178 "{}() requires all array elements to be numbers",
2179 func_name
2180 )));
2181 }
2182 }
2183 }
2184 Ok(())
2185 }
2186 recurse(arr, func_name, &mut f)
2187 }
2188
2189 /// Count elements in a potentially nested array without cloning.
2190 fn count_numeric(arr: &[JValue], func_name: &str) -> Result<usize, EvaluatorError> {
2191 let mut count = 0usize;
2192 for_each_numeric(arr, func_name, |_| count += 1)?;
2193 Ok(count)
2194 }
2195
2196 pub fn sum(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2197 if arr.is_empty() {
2198 return Ok(JValue::from(0i64));
2199 }
2200 let mut total = 0.0f64;
2201 for_each_numeric(arr, "sum", |n| total += n)?;
2202 Ok(JValue::Number(total))
2203 }
2204
2205 pub fn max(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2206 if arr.is_empty() {
2207 return Ok(JValue::Null);
2208 }
2209 let mut max_val = f64::NEG_INFINITY;
2210 for_each_numeric(arr, "max", |n| {
2211 if n > max_val {
2212 max_val = n;
2213 }
2214 })?;
2215 Ok(JValue::Number(max_val))
2216 }
2217
2218 pub fn min(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2219 if arr.is_empty() {
2220 return Ok(JValue::Null);
2221 }
2222 let mut min_val = f64::INFINITY;
2223 for_each_numeric(arr, "min", |n| {
2224 if n < min_val {
2225 min_val = n;
2226 }
2227 })?;
2228 Ok(JValue::Number(min_val))
2229 }
2230
2231 pub fn average(arr: &[JValue]) -> Result<JValue, EvaluatorError> {
2232 if arr.is_empty() {
2233 return Ok(JValue::Null);
2234 }
2235 let mut total = 0.0f64;
2236 let count = count_numeric(arr, "average")?;
2237 for_each_numeric(arr, "average", |n| total += n)?;
2238 Ok(JValue::Number(total / count as f64))
2239 }
2240}
2241
2242/// Evaluator errors
2243#[derive(Error, Debug)]
2244pub enum EvaluatorError {
2245 #[error("Type error: {0}")]
2246 TypeError(String),
2247
2248 #[error("Reference error: {0}")]
2249 ReferenceError(String),
2250
2251 #[error("Evaluation error: {0}")]
2252 EvaluationError(String),
2253}
2254
2255impl From<crate::functions::FunctionError> for EvaluatorError {
2256 fn from(e: crate::functions::FunctionError) -> Self {
2257 EvaluatorError::EvaluationError(e.to_string())
2258 }
2259}
2260
2261impl From<crate::datetime::DateTimeError> for EvaluatorError {
2262 fn from(e: crate::datetime::DateTimeError) -> Self {
2263 EvaluatorError::EvaluationError(e.to_string())
2264 }
2265}
2266
2267/// Result of evaluating a lambda body that may be a tail call
2268/// Used for trampoline-based tail call optimization
2269enum LambdaResult {
2270 /// Final value - evaluation is complete
2271 JValue(JValue),
2272 /// Tail call - need to continue with another lambda invocation
2273 TailCall {
2274 /// The lambda to call (boxed to reduce enum size)
2275 lambda: Box<StoredLambda>,
2276 /// Arguments for the call
2277 args: Vec<JValue>,
2278 /// Data context for the call
2279 data: JValue,
2280 },
2281}
2282
2283/// Lambda storage
2284/// Stores the AST of a lambda function along with its parameters, optional signature,
2285/// and captured environment for closures
2286#[derive(Clone, Debug)]
2287pub struct StoredLambda {
2288 pub params: Vec<String>,
2289 pub body: AstNode,
2290 /// Pre-compiled body for use in tight inner loops (HOF fast path).
2291 /// `None` if the body is not compilable (transform, partial-app, thunk, etc.).
2292 pub(crate) compiled_body: Option<CompiledExpr>,
2293 pub signature: Option<String>,
2294 /// Captured environment bindings for closures
2295 pub captured_env: HashMap<String, JValue>,
2296 /// Captured data context for lexical scoping of bare field names
2297 pub captured_data: Option<JValue>,
2298 /// Whether this lambda's body contains tail calls that can be optimized
2299 pub thunk: bool,
2300}
2301
2302/// A single scope in the scope stack
2303struct Scope {
2304 bindings: HashMap<String, JValue>,
2305 lambdas: HashMap<String, StoredLambda>,
2306}
2307
2308impl Scope {
2309 fn new() -> Self {
2310 Scope {
2311 bindings: HashMap::new(),
2312 lambdas: HashMap::new(),
2313 }
2314 }
2315}
2316
2317/// Evaluation context
2318///
2319/// Holds variable bindings and other state needed during evaluation.
2320/// Uses a scope stack for efficient push/pop instead of clone/restore.
2321pub struct Context {
2322 scope_stack: Vec<Scope>,
2323 parent_data: Option<JValue>,
2324}
2325
2326impl Context {
2327 pub fn new() -> Self {
2328 Context {
2329 scope_stack: vec![Scope::new()],
2330 parent_data: None,
2331 }
2332 }
2333
2334 /// Push a new scope onto the stack
2335 fn push_scope(&mut self) {
2336 self.scope_stack.push(Scope::new());
2337 }
2338
2339 /// Pop the top scope from the stack
2340 fn pop_scope(&mut self) {
2341 if self.scope_stack.len() > 1 {
2342 self.scope_stack.pop();
2343 }
2344 }
2345
2346 /// Pop scope but preserve specified lambdas by moving them to the current top scope
2347 fn pop_scope_preserving_lambdas(&mut self, lambda_ids: &[String]) {
2348 if self.scope_stack.len() > 1 {
2349 let popped = self.scope_stack.pop().unwrap();
2350 if !lambda_ids.is_empty() {
2351 let top = self.scope_stack.last_mut().unwrap();
2352 for id in lambda_ids {
2353 if let Some(stored) = popped.lambdas.get(id) {
2354 top.lambdas.insert(id.clone(), stored.clone());
2355 }
2356 }
2357 }
2358 }
2359 }
2360
2361 /// Clear all bindings and lambdas in the top scope without deallocating
2362 fn clear_current_scope(&mut self) {
2363 let top = self.scope_stack.last_mut().unwrap();
2364 top.bindings.clear();
2365 top.lambdas.clear();
2366 }
2367
2368 pub fn bind(&mut self, name: String, value: JValue) {
2369 self.scope_stack
2370 .last_mut()
2371 .unwrap()
2372 .bindings
2373 .insert(name, value);
2374 }
2375
2376 pub fn bind_lambda(&mut self, name: String, lambda: StoredLambda) {
2377 self.scope_stack
2378 .last_mut()
2379 .unwrap()
2380 .lambdas
2381 .insert(name, lambda);
2382 }
2383
2384 pub fn unbind(&mut self, name: &str) {
2385 // Remove from top scope only
2386 let top = self.scope_stack.last_mut().unwrap();
2387 top.bindings.remove(name);
2388 top.lambdas.remove(name);
2389 }
2390
2391 pub fn lookup(&self, name: &str) -> Option<&JValue> {
2392 // Walk scope stack from top to bottom
2393 for scope in self.scope_stack.iter().rev() {
2394 if let Some(value) = scope.bindings.get(name) {
2395 return Some(value);
2396 }
2397 }
2398 None
2399 }
2400
2401 pub fn lookup_lambda(&self, name: &str) -> Option<&StoredLambda> {
2402 // Walk scope stack from top to bottom
2403 for scope in self.scope_stack.iter().rev() {
2404 if let Some(lambda) = scope.lambdas.get(name) {
2405 return Some(lambda);
2406 }
2407 }
2408 None
2409 }
2410
2411 pub fn set_parent(&mut self, data: JValue) {
2412 self.parent_data = Some(data);
2413 }
2414
2415 pub fn get_parent(&self) -> Option<&JValue> {
2416 self.parent_data.as_ref()
2417 }
2418
2419 /// Collect all bindings across all scopes (for environment capture).
2420 /// Higher scopes shadow lower scopes.
2421 fn all_bindings(&self) -> HashMap<String, JValue> {
2422 let mut result = HashMap::new();
2423 for scope in &self.scope_stack {
2424 for (k, v) in &scope.bindings {
2425 result.insert(k.clone(), v.clone());
2426 }
2427 }
2428 result
2429 }
2430}
2431
2432impl Default for Context {
2433 fn default() -> Self {
2434 Self::new()
2435 }
2436}
2437
2438/// Strip any lingering tuple-stream wrapper objects (`{"@":.., "__tuple__":true,
2439/// ...}`) from a value about to leave the evaluator.
2440///
2441/// `%`/`@`/`#` are implemented internally by wrapping each element of a path
2442/// step's result in a tuple object (see `create_tuple_stream`) so downstream
2443/// steps can resolve ancestor/focus/index bindings. Ordinarily an intermediate
2444/// path step consumes and re-wraps these as evaluation proceeds, but the
2445/// *final* result of an `evaluate()` call can still be tuple-wrapped — either
2446/// because the tuple-producing expression itself is the whole result (a bare
2447/// `#`/`@`/`%` path), or because it's nested inside object/array construction
2448/// (e.g. `{"skus": Product[%.OrderID=...].SKU}` or `[items#$i]`) where the
2449/// wrapper ends up embedded in a field value or array element rather than at
2450/// the top level. This recurses through both array elements and (non-tuple)
2451/// object field values so both shapes are cleaned up, not just a bare
2452/// top-level tuple array.
2453/// Merge a group of tuple wrappers into a single tuple, appending each key's
2454/// values across the group. Mirrors jsonata-js `reduceTupleStream`
2455/// (`Object.assign(result, tuple[0]); result[prop] = append(result[prop], ...)`):
2456/// a key present in one tuple stays a scalar; a key present in several becomes an
2457/// array of the collected values (used by group-by value evaluation so a group
2458/// of N tuples exposes `@` as the N collected `@` values and each `$focus` as the
2459/// N collected focus values).
2460fn reduce_tuple_stream(group: &[JValue]) -> IndexMap<String, JValue> {
2461 fn append(acc: Option<JValue>, v: JValue) -> JValue {
2462 match acc {
2463 None => v,
2464 Some(a) => {
2465 let mut out: Vec<JValue> = match a {
2466 JValue::Array(arr) => arr.iter().cloned().collect(),
2467 other => vec![other],
2468 };
2469 match v {
2470 JValue::Array(arr) => out.extend(arr.iter().cloned()),
2471 other => out.push(other),
2472 }
2473 JValue::array(out)
2474 }
2475 }
2476 }
2477 let mut result: IndexMap<String, JValue> = IndexMap::new();
2478 for tuple in group {
2479 if let JValue::Object(obj) = tuple {
2480 for (k, v) in obj.iter() {
2481 if k == "__tuple__" {
2482 result.insert(k.clone(), v.clone());
2483 continue;
2484 }
2485 let merged = append(result.shift_remove(k), v.clone());
2486 result.insert(k.clone(), merged);
2487 }
2488 }
2489 }
2490 result
2491}
2492
2493fn unwrap_tuple_output(value: JValue) -> JValue {
2494 match value {
2495 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => obj
2496 .get("@")
2497 .cloned()
2498 .map(unwrap_tuple_output)
2499 .unwrap_or(JValue::Undefined),
2500 JValue::Object(obj) => {
2501 let mut new_map = IndexMap::with_capacity(obj.len());
2502 for (k, v) in obj.iter() {
2503 new_map.insert(k.clone(), unwrap_tuple_output(v.clone()));
2504 }
2505 JValue::object(new_map)
2506 }
2507 JValue::Array(arr) => JValue::array(arr.iter().cloned().map(unwrap_tuple_output).collect()),
2508 other => other,
2509 }
2510}
2511
2512/// Guard returned by [`Evaluator::bind_tuple_keys`]: remembers, for each
2513/// tuple-carried `$name`/`!label` key that was just bound into scope, what
2514/// (if anything) was bound under that name beforehand. `restore` puts the
2515/// prior value back -- or removes the binding entirely if there wasn't
2516/// one -- rather than unconditionally unbinding, so a tuple key that
2517/// happens to share a name with a live outer `:=` binding in the same
2518/// scope frame doesn't get permanently deleted once the tuple-row
2519/// evaluation finishes.
2520struct TupleKeyBindings {
2521 saved: Vec<(String, Option<JValue>)>,
2522}
2523
2524impl TupleKeyBindings {
2525 /// True if `name` was one of the keys this guard bound (used by callers
2526 /// that need to know whether a given tuple key is already in scope
2527 /// before binding it a second time under a different role, e.g.
2528 /// `create_tuple_stream`'s ancestor-label handling).
2529 fn contains(&self, name: &str) -> bool {
2530 self.saved.iter().any(|(n, _)| n == name)
2531 }
2532
2533 fn restore(self, evaluator: &mut Evaluator) {
2534 for (name, prior) in self.saved {
2535 match prior {
2536 Some(value) => evaluator.context.bind(name, value),
2537 None => evaluator.context.unbind(&name),
2538 }
2539 }
2540 }
2541}
2542
2543/// Evaluator for JSONata expressions
2544pub struct Evaluator {
2545 context: Context,
2546 recursion_depth: usize,
2547 max_recursion_depth: usize,
2548 /// Monotonic counter for generating unique lambda IDs. Each evaluation of a
2549 /// Lambda AST node creates a new closure *instance* and must get a fresh ID -
2550 /// using the AST node's pointer address (as before) collided whenever the same
2551 /// lambda expression was evaluated more than once (e.g. each level of Y-combinator
2552 /// or other repeated recursion), aliasing unrelated closures that shared an id.
2553 next_lambda_id: u64,
2554 /// Set whenever `create_tuple_stream` builds a `{"@":.., "__tuple__":true}`
2555 /// wrapper during this top-level `evaluate()` call. Reset at the start of
2556 /// `evaluate()` and checked at the end to decide whether the (recursive,
2557 /// O(result size)) tuple-unwrap pass is needed before returning to the
2558 /// caller — keeps the vast majority of evaluations, which never touch
2559 /// `%`/`@`/`#`, at zero added cost.
2560 tuple_stream_created: bool,
2561 /// When true, `evaluate_path` skips its end-of-path `@`-projection and returns
2562 /// the raw `{@, $var, !label, __tuple__}` tuple wrappers. Set (saved/restored)
2563 /// by the two consumers that read those carried bindings directly from the
2564 /// wrappers: a `Sort` node evaluating its tuple-carrying input path (sort
2565 /// terms reference `%`/`$focus`), and an `ObjectTransform` (group-by)
2566 /// evaluating its input path (key/value expressions read `$focus` off the
2567 /// wrapper). Mirrors jsonata-js keeping `path.tuple` for such a path instead
2568 /// of projecting each tuple's `@`.
2569 keep_tuple_stream: bool,
2570}
2571
2572impl Evaluator {
2573 pub fn new() -> Self {
2574 Evaluator {
2575 context: Context::new(),
2576 recursion_depth: 0,
2577 // Limit recursion depth to prevent stack overflow
2578 // True TCO would allow deeper recursion but requires parser-level thunk marking
2579 max_recursion_depth: 302,
2580 next_lambda_id: 0,
2581 tuple_stream_created: false,
2582 keep_tuple_stream: false,
2583 }
2584 }
2585
2586 pub fn with_context(context: Context) -> Self {
2587 Evaluator {
2588 context,
2589 recursion_depth: 0,
2590 max_recursion_depth: 302,
2591 next_lambda_id: 0,
2592 tuple_stream_created: false,
2593 keep_tuple_stream: false,
2594 }
2595 }
2596
2597 /// Allocate a fresh, process-unique-per-Evaluator id for a new lambda instance.
2598 fn fresh_lambda_id(&mut self) -> u64 {
2599 let id = self.next_lambda_id;
2600 self.next_lambda_id += 1;
2601 id
2602 }
2603
2604 /// Invoke a stored lambda with its captured environment and data.
2605 /// This is the standard way to call a StoredLambda, handling the
2606 /// captured_env and captured_data extraction boilerplate.
2607 fn invoke_stored_lambda(
2608 &mut self,
2609 stored: &StoredLambda,
2610 args: &[JValue],
2611 data: &JValue,
2612 ) -> Result<JValue, EvaluatorError> {
2613 // Compiled fast path: skip scope push/pop and tree-walking for simple lambdas.
2614 // Conditions: has compiled body, no signature (can't skip validation), no thunk,
2615 // and no captured lambda/builtin values (those require Context for runtime lookup).
2616 if let Some(ref ce) = stored.compiled_body {
2617 if stored.signature.is_none()
2618 && !stored.thunk
2619 && !stored
2620 .captured_env
2621 .values()
2622 .any(|v| matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. }))
2623 {
2624 let call_data = stored.captured_data.as_ref().unwrap_or(data);
2625 let vars: HashMap<&str, &JValue> = stored
2626 .params
2627 .iter()
2628 .zip(args.iter())
2629 .map(|(p, v)| (p.as_str(), v))
2630 .chain(stored.captured_env.iter().map(|(k, v)| (k.as_str(), v)))
2631 .collect();
2632 return eval_compiled(ce, call_data, Some(&vars));
2633 }
2634 }
2635
2636 let captured_env = if stored.captured_env.is_empty() {
2637 None
2638 } else {
2639 Some(&stored.captured_env)
2640 };
2641 let captured_data = stored.captured_data.as_ref();
2642 self.invoke_lambda_with_env(
2643 &stored.params,
2644 &stored.body,
2645 stored.signature.as_ref(),
2646 args,
2647 data,
2648 captured_env,
2649 captured_data,
2650 stored.thunk,
2651 )
2652 }
2653
2654 /// Look up a StoredLambda from a JValue that may be a lambda marker.
2655 /// Returns the cloned StoredLambda if the value is a JValue::Lambda variant
2656 /// with a valid lambda_id that references a stored lambda.
2657 fn lookup_lambda_from_value(&self, value: &JValue) -> Option<StoredLambda> {
2658 if let JValue::Lambda { lambda_id, .. } = value {
2659 return self.context.lookup_lambda(lambda_id).cloned();
2660 }
2661 None
2662 }
2663
2664 /// Get the number of parameters a callback function expects by inspecting its AST.
2665 /// This is used to avoid passing unnecessary arguments to callbacks in HOF functions.
2666 /// Returns the parameter count, or usize::MAX if unable to determine (meaning pass all args).
2667 fn get_callback_param_count(&self, func_node: &AstNode) -> usize {
2668 match func_node {
2669 AstNode::Lambda { params, .. } => params.len(),
2670 AstNode::Variable(var_name) => {
2671 // Check if this variable holds a stored lambda
2672 if let Some(stored_lambda) = self.context.lookup_lambda(var_name) {
2673 return stored_lambda.params.len();
2674 }
2675 // Also check if it's a lambda value in bindings (e.g., from partial application)
2676 if let Some(value) = self.context.lookup(var_name) {
2677 if let Some(stored_lambda) = self.lookup_lambda_from_value(value) {
2678 return stored_lambda.params.len();
2679 }
2680 }
2681 // Unknown, return max to be safe
2682 usize::MAX
2683 }
2684 AstNode::Function { .. } => {
2685 // For function references, we can't easily determine param count
2686 // Return max to be safe
2687 usize::MAX
2688 }
2689 _ => usize::MAX,
2690 }
2691 }
2692
2693 /// Specialized sort using pre-extracted keys (Schwartzian transform).
2694 /// Extracts sort keys once (N lookups), then sorts by comparing keys directly,
2695 /// avoiding O(N log N) hash lookups during comparisons.
2696 fn merge_sort_specialized(arr: &mut [JValue], spec: &SpecializedSortComparator) {
2697 if arr.len() <= 1 {
2698 return;
2699 }
2700
2701 // Phase 1: Extract sort keys -- one IndexMap lookup per element
2702 let keys: Vec<SortKey> = arr
2703 .iter()
2704 .map(|item| match item {
2705 JValue::Object(obj) => match obj.get(&spec.field) {
2706 Some(JValue::Number(n)) => SortKey::Num(*n),
2707 Some(JValue::String(s)) => SortKey::Str(s.clone()),
2708 _ => SortKey::None,
2709 },
2710 _ => SortKey::None,
2711 })
2712 .collect();
2713
2714 // Phase 2: Build index permutation sorted by pre-extracted keys
2715 let mut perm: Vec<usize> = (0..arr.len()).collect();
2716 perm.sort_by(|&a, &b| compare_sort_keys(&keys[a], &keys[b], spec.descending));
2717
2718 // Phase 3: Apply permutation in-place via cycle-following
2719 let mut placed = vec![false; arr.len()];
2720 for i in 0..arr.len() {
2721 if placed[i] || perm[i] == i {
2722 continue;
2723 }
2724 let mut j = i;
2725 loop {
2726 let target = perm[j];
2727 placed[j] = true;
2728 if target == i {
2729 break;
2730 }
2731 arr.swap(j, target);
2732 j = target;
2733 }
2734 }
2735 }
2736
2737 /// Merge sort implementation using a comparator function.
2738 /// This replaces the O(n²) bubble sort for better performance on large arrays.
2739 /// The comparator returns true if the first element should come AFTER the second.
2740 fn merge_sort_with_comparator(
2741 &mut self,
2742 arr: &mut [JValue],
2743 comparator: &AstNode,
2744 data: &JValue,
2745 ) -> Result<(), EvaluatorError> {
2746 if arr.len() <= 1 {
2747 return Ok(());
2748 }
2749
2750 // Try specialized fast path for simple field comparisons like
2751 // function($l, $r) { $l.price > $r.price }
2752 if let AstNode::Lambda { params, body, .. } = comparator {
2753 if params.len() >= 2 {
2754 if let Some(spec) = try_specialize_sort_comparator(body, ¶ms[0], ¶ms[1]) {
2755 Self::merge_sort_specialized(arr, &spec);
2756 return Ok(());
2757 }
2758 }
2759 }
2760
2761 let mid = arr.len() / 2;
2762
2763 // Sort left half
2764 self.merge_sort_with_comparator(&mut arr[..mid], comparator, data)?;
2765
2766 // Sort right half
2767 self.merge_sort_with_comparator(&mut arr[mid..], comparator, data)?;
2768
2769 // Merge the sorted halves
2770 let mut temp = Vec::with_capacity(arr.len());
2771 let (left, right) = arr.split_at(mid);
2772
2773 let mut i = 0;
2774 let mut j = 0;
2775
2776 // For lambda comparators, use a reusable scope to avoid
2777 // push_scope/pop_scope per comparison (~n log n total comparisons)
2778 if let AstNode::Lambda { params, body, .. } = comparator {
2779 if params.len() >= 2 {
2780 // Pre-clone param names once outside the loop
2781 let param0 = params[0].clone();
2782 let param1 = params[1].clone();
2783 self.context.push_scope();
2784 while i < left.len() && j < right.len() {
2785 // Reuse scope: clear and rebind instead of push/pop
2786 self.context.clear_current_scope();
2787 self.context.bind(param0.clone(), left[i].clone());
2788 self.context.bind(param1.clone(), right[j].clone());
2789
2790 let cmp_result = self.evaluate_internal(body, data)?;
2791
2792 if self.is_truthy(&cmp_result) {
2793 temp.push(right[j].clone());
2794 j += 1;
2795 } else {
2796 temp.push(left[i].clone());
2797 i += 1;
2798 }
2799 }
2800 self.context.pop_scope();
2801 } else {
2802 // Unexpected param count - fall back to generic path
2803 while i < left.len() && j < right.len() {
2804 let cmp_result = self.apply_function(
2805 comparator,
2806 &[left[i].clone(), right[j].clone()],
2807 data,
2808 )?;
2809 if self.is_truthy(&cmp_result) {
2810 temp.push(right[j].clone());
2811 j += 1;
2812 } else {
2813 temp.push(left[i].clone());
2814 i += 1;
2815 }
2816 }
2817 }
2818 } else {
2819 // Non-lambda comparator: use generic apply_function path
2820 while i < left.len() && j < right.len() {
2821 let cmp_result =
2822 self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
2823 if self.is_truthy(&cmp_result) {
2824 temp.push(right[j].clone());
2825 j += 1;
2826 } else {
2827 temp.push(left[i].clone());
2828 i += 1;
2829 }
2830 }
2831 }
2832
2833 // Copy remaining elements
2834 temp.extend_from_slice(&left[i..]);
2835 temp.extend_from_slice(&right[j..]);
2836
2837 // Copy back to original array (can't use copy_from_slice since JValue is not Copy)
2838 for (i, val) in temp.into_iter().enumerate() {
2839 arr[i] = val;
2840 }
2841
2842 Ok(())
2843 }
2844
2845 /// Evaluate an AST node against data
2846 ///
2847 /// This is the main entry point for evaluation. It sets up the parent context
2848 /// to be the root data if not already set.
2849 ///
2850 /// Also the single choke point for stripping any lingering tuple-stream
2851 /// wrapper objects (`{"@":.., "__tuple__":true, ...}`) from the result before
2852 /// it reaches the caller — `%`/`@`/`#` are implemented internally via a
2853 /// tuple-stream representation (see `create_tuple_stream`), and without this
2854 /// a bare (or object/array-nested) tuple-producing expression would leak
2855 /// that internal representation into user-visible output instead of the
2856 /// plain value.
2857 pub fn evaluate(&mut self, node: &AstNode, data: &JValue) -> Result<JValue, EvaluatorError> {
2858 // Set parent context to root data if not already set
2859 if self.context.get_parent().is_none() {
2860 self.context.set_parent(data.clone());
2861 }
2862
2863 self.tuple_stream_created = false;
2864 let result = self.evaluate_internal(node, data)?;
2865 Ok(if self.tuple_stream_created {
2866 unwrap_tuple_output(result)
2867 } else {
2868 result
2869 })
2870 }
2871
2872 /// Fast evaluation for leaf nodes that don't need recursion tracking.
2873 /// Returns Some for literals, simple field access on objects, and simple variable lookups.
2874 /// Returns None for anything requiring the full evaluator.
2875 #[inline(always)]
2876 fn evaluate_leaf(
2877 &mut self,
2878 node: &AstNode,
2879 data: &JValue,
2880 ) -> Option<Result<JValue, EvaluatorError>> {
2881 match node {
2882 AstNode::String(s) => Some(Ok(JValue::string(s.clone()))),
2883 AstNode::Number(n) => {
2884 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2885 Some(Ok(JValue::from(*n as i64)))
2886 } else {
2887 Some(Ok(JValue::Number(*n)))
2888 }
2889 }
2890 AstNode::Boolean(b) => Some(Ok(JValue::Bool(*b))),
2891 AstNode::Null => Some(Ok(JValue::Null)),
2892 AstNode::Undefined => Some(Ok(JValue::Undefined)),
2893 AstNode::Name(field_name) => match data {
2894 // Array mapping and other cases need full evaluator
2895 JValue::Object(obj) => Some(Ok(obj
2896 .get(field_name)
2897 .cloned()
2898 .unwrap_or(JValue::Undefined))),
2899 _ => None,
2900 },
2901 AstNode::Variable(name) if !name.is_empty() => {
2902 // Simple variable lookup — only fast-path when no tuple data
2903 if let JValue::Object(obj) = data {
2904 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
2905 return None; // Tuple data needs full evaluator
2906 }
2907 }
2908 // May be a lambda/builtin — needs full evaluator if None
2909 self.context.lookup(name).map(|value| Ok(value.clone()))
2910 }
2911 _ => None,
2912 }
2913 }
2914
2915 /// Internal evaluation method
2916 fn evaluate_internal(
2917 &mut self,
2918 node: &AstNode,
2919 data: &JValue,
2920 ) -> Result<JValue, EvaluatorError> {
2921 // Fast path for leaf nodes — skip recursion tracking overhead
2922 if let Some(result) = self.evaluate_leaf(node, data) {
2923 return result;
2924 }
2925
2926 // Check recursion depth to prevent stack overflow
2927 self.recursion_depth += 1;
2928 if self.recursion_depth > self.max_recursion_depth {
2929 self.recursion_depth -= 1;
2930 return Err(EvaluatorError::EvaluationError(format!(
2931 "U1001: Stack overflow - maximum recursion depth ({}) exceeded",
2932 self.max_recursion_depth
2933 )));
2934 }
2935
2936 // The soft depth counter above is calibrated against a comfortably
2937 // large native stack. Hosts with a much smaller default thread stack
2938 // (notably Windows, ~1MB vs Linux's ~8MB) can exhaust the *real*
2939 // stack well before this counter trips, crashing the process instead
2940 // of returning U1001 (see GitHub issue #34). stacker::maybe_grow
2941 // transparently swaps in a bigger stack segment when headroom is
2942 // low, so this stays a no-op cost on the common shallow path.
2943 const RED_ZONE: usize = 128 * 1024;
2944 const GROW_STACK_SIZE: usize = 8 * 1024 * 1024;
2945 let result = stacker::maybe_grow(RED_ZONE, GROW_STACK_SIZE, || {
2946 self.evaluate_internal_impl(node, data)
2947 });
2948
2949 self.recursion_depth -= 1;
2950 result
2951 }
2952
2953 /// Internal evaluation implementation (separated to allow depth tracking)
2954 fn evaluate_internal_impl(
2955 &mut self,
2956 node: &AstNode,
2957 data: &JValue,
2958 ) -> Result<JValue, EvaluatorError> {
2959 match node {
2960 AstNode::String(s) => Ok(JValue::string(s.clone())),
2961
2962 // Name nodes represent field access on the current data
2963 AstNode::Name(field_name) => {
2964 match data {
2965 JValue::Object(obj) => {
2966 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
2967 }
2968 JValue::Array(arr) => {
2969 // Map over array
2970 let mut result = Vec::new();
2971 for item in arr.iter() {
2972 if let JValue::Object(obj) = item {
2973 if let Some(val) = obj.get(field_name) {
2974 result.push(val.clone());
2975 }
2976 }
2977 }
2978 if result.is_empty() {
2979 Ok(JValue::Undefined)
2980 } else if result.len() == 1 {
2981 Ok(result.into_iter().next().unwrap())
2982 } else {
2983 Ok(JValue::array(result))
2984 }
2985 }
2986 _ => Ok(JValue::Undefined),
2987 }
2988 }
2989
2990 AstNode::Number(n) => {
2991 // Preserve integer-ness: if the number is a whole number, create an integer JValue
2992 if n.fract() == 0.0 && n.is_finite() && n.abs() < (1i64 << 53) as f64 {
2993 // It's a whole number that can be represented as i64
2994 Ok(JValue::from(*n as i64))
2995 } else {
2996 Ok(JValue::Number(*n))
2997 }
2998 }
2999 AstNode::Boolean(b) => Ok(JValue::Bool(*b)),
3000 AstNode::Null => Ok(JValue::Null),
3001 AstNode::Undefined => Ok(JValue::Undefined),
3002 AstNode::Placeholder => {
3003 // Placeholders should only appear as function arguments
3004 // If we reach here, it's an error
3005 Err(EvaluatorError::EvaluationError(
3006 "Placeholder '?' can only be used as a function argument".to_string(),
3007 ))
3008 }
3009 AstNode::Regex { pattern, flags } => {
3010 // Return a regex object as a special JSON value
3011 // This will be recognized by functions like $split, $match, $replace
3012 Ok(JValue::regex(pattern.as_str(), flags.as_str()))
3013 }
3014
3015 AstNode::Variable(name) => {
3016 // Special case: $ alone (empty name) refers to current context
3017 // First check if $ is bound in the context (for closures that captured $)
3018 // Otherwise, use the data parameter
3019 if name.is_empty() {
3020 if let Some(value) = self.context.lookup("$") {
3021 return Ok(value.clone());
3022 }
3023 // If data is a tuple, return the @ value
3024 if let JValue::Object(obj) = data {
3025 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3026 if let Some(inner) = obj.get("@") {
3027 return Ok(inner.clone());
3028 }
3029 }
3030 }
3031 return Ok(data.clone());
3032 }
3033
3034 // Check variable bindings FIRST
3035 // This allows function parameters to shadow outer lambdas with the same name
3036 // Critical for Y-combinator pattern: function($g){$g($g)} where $g shadows outer $g
3037 if let Some(value) = self.context.lookup(name) {
3038 return Ok(value.clone());
3039 }
3040
3041 // Check tuple bindings in data (for index binding operator #$var)
3042 // When iterating over a tuple stream, $var can reference the bound index
3043 if let JValue::Object(obj) = data {
3044 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3045 // Check for the variable in tuple bindings (stored as "$name")
3046 let binding_key = format!("${}", name);
3047 if let Some(binding_value) = obj.get(&binding_key) {
3048 return Ok(binding_value.clone());
3049 }
3050 }
3051 }
3052
3053 // Then check if this is a stored lambda (user-defined functions)
3054 if let Some(stored_lambda) = self.context.lookup_lambda(name) {
3055 // Return a lambda representation that can be passed to higher-order functions
3056 // Include _lambda_id pointing to the stored lambda so it can be found
3057 // when captured in closures
3058 let lambda_repr = JValue::lambda(
3059 name.as_str(),
3060 stored_lambda.params.clone(),
3061 Some(name.to_string()),
3062 stored_lambda.signature.clone(),
3063 );
3064 return Ok(lambda_repr);
3065 }
3066
3067 // Check if this is a built-in function reference (only if not shadowed)
3068 if self.is_builtin_function(name) {
3069 // Return a marker for built-in functions
3070 // This allows built-in functions to be passed to higher-order functions
3071 let builtin_repr = JValue::builtin(name.as_str());
3072 return Ok(builtin_repr);
3073 }
3074
3075 // Undefined variable - return null (undefined in JSONata semantics)
3076 // This allows expressions like `$not(undefined_var)` to return undefined
3077 // and comparisons like `3 > $undefined` to return undefined
3078 Ok(JValue::Null)
3079 }
3080
3081 AstNode::ParentVariable(name) => {
3082 // Special case: $$ alone (empty name) refers to parent/root context
3083 if name.is_empty() {
3084 return self.context.get_parent().cloned().ok_or_else(|| {
3085 EvaluatorError::ReferenceError("Parent context not available".to_string())
3086 });
3087 }
3088
3089 // For $$name, we need to evaluate name against parent context
3090 // This is similar to $.name but using parent data
3091 let parent_data = self.context.get_parent().ok_or_else(|| {
3092 EvaluatorError::ReferenceError("Parent context not available".to_string())
3093 })?;
3094
3095 // Access field on parent context
3096 match parent_data {
3097 JValue::Object(obj) => Ok(obj.get(name).cloned().unwrap_or(JValue::Null)),
3098 _ => Ok(JValue::Null),
3099 }
3100 }
3101
3102 AstNode::Path { steps } => self.evaluate_path(steps, data),
3103
3104 AstNode::Binary { op, lhs, rhs } => self.evaluate_binary_op(*op, lhs, rhs, data),
3105
3106 AstNode::Unary { op, operand } => self.evaluate_unary_op(*op, operand, data),
3107
3108 // Array constructor - JSONata semantics:
3109 AstNode::Array(elements) => {
3110 // - If element is itself an array constructor [...], keep it nested
3111 // - Otherwise, if element evaluates to an array, flatten it
3112 // - Undefined values are excluded
3113 let mut result = Vec::with_capacity(elements.len());
3114 for element in elements {
3115 // Check if this element is itself an explicit array constructor
3116 let is_array_constructor = matches!(element, AstNode::Array(_));
3117
3118 let value = self.evaluate_internal(element, data)?;
3119
3120 // Skip undefined values in array constructors
3121 // Note: explicit null is preserved, only undefined (no value) is filtered
3122 if value.is_undefined() {
3123 continue;
3124 }
3125
3126 if is_array_constructor {
3127 // Explicit array constructor - keep nested
3128 result.push(value);
3129 } else if let JValue::Array(arr) = value {
3130 // Non-array-constructor that evaluated to array - flatten it
3131 result.extend(arr.iter().cloned());
3132 } else {
3133 // Non-array value - add as-is
3134 result.push(value);
3135 }
3136 }
3137 Ok(JValue::array(result))
3138 }
3139
3140 AstNode::Object(pairs) => {
3141 let mut result = IndexMap::with_capacity(pairs.len());
3142
3143 // Check if all keys are string literals — can skip D1009 HashMap
3144 let all_literal_keys = pairs.iter().all(|(k, _)| matches!(k, AstNode::String(_)));
3145
3146 if all_literal_keys {
3147 // Fast path: literal keys, no need for D1009 tracking
3148 for (key_node, value_node) in pairs.iter() {
3149 let key = match key_node {
3150 AstNode::String(s) => s,
3151 _ => unreachable!(),
3152 };
3153 let value = self.evaluate_internal(value_node, data)?;
3154 if value.is_undefined() {
3155 continue;
3156 }
3157 result.insert(key.clone(), value);
3158 }
3159 } else {
3160 let mut key_sources: HashMap<String, usize> = HashMap::new();
3161 for (pair_index, (key_node, value_node)) in pairs.iter().enumerate() {
3162 let key = match self.evaluate_internal(key_node, data)? {
3163 JValue::String(s) => s,
3164 JValue::Null => continue,
3165 other => {
3166 if other.is_undefined() {
3167 continue;
3168 }
3169 return Err(EvaluatorError::TypeError(format!(
3170 "Object key must be a string, got: {:?}",
3171 other
3172 )));
3173 }
3174 };
3175
3176 if let Some(&existing_idx) = key_sources.get(&*key) {
3177 if existing_idx != pair_index {
3178 return Err(EvaluatorError::EvaluationError(format!(
3179 "D1009: Multiple key expressions evaluate to same key: {}",
3180 key
3181 )));
3182 }
3183 }
3184 key_sources.insert(key.to_string(), pair_index);
3185
3186 let value = self.evaluate_internal(value_node, data)?;
3187 if value.is_undefined() {
3188 continue;
3189 }
3190 result.insert(key.to_string(), value);
3191 }
3192 }
3193 Ok(JValue::object(result))
3194 }
3195
3196 // Object transform: group items by key, then evaluate value once per group
3197 AstNode::ObjectTransform { input, pattern } => {
3198 // Evaluate the input expression. Keep tuple wrappers alive so the
3199 // group-by key/value expressions can read the carried `$focus`
3200 // bindings off each wrapper (e.g. `...@$e...{ $e.FirstName: ... }`).
3201 let saved_keep = self.keep_tuple_stream;
3202 self.keep_tuple_stream = true;
3203 let input_value = self.evaluate_internal(input, data);
3204 self.keep_tuple_stream = saved_keep;
3205 let input_value = input_value?;
3206
3207 // If input is undefined, return undefined (not empty object)
3208 if input_value.is_undefined() {
3209 return Ok(JValue::Undefined);
3210 }
3211
3212 // Handle array input - process each item
3213 let items: Vec<JValue> = match input_value {
3214 JValue::Array(ref arr) => (**arr).clone(),
3215 JValue::Null => return Ok(JValue::Null),
3216 other => vec![other],
3217 };
3218
3219 // If array is empty, add undefined to enable literal JSON object generation
3220 let items = if items.is_empty() {
3221 vec![JValue::Undefined]
3222 } else {
3223 items
3224 };
3225
3226 // Grouping over a tuple stream ("reduce" mode, mirroring
3227 // jsonata-js evaluateGroupExpression): each item is a
3228 // `{@, $var, !label, __tuple__}` wrapper. The key/value
3229 // expressions are evaluated against the tuple's `@` value with the
3230 // carried focus/index/ancestor keys bound into scope (so
3231 // `...@$e...{ $e.FirstName: Phone[type='mobile'].number }` reads
3232 // `$e` AND resolves the relative `Phone` against the Contact `@`),
3233 // and grouped tuples are reduced (per-key values appended) before
3234 // the value expression sees them.
3235 let reduce = items.first().is_some_and(|it| {
3236 matches!(it, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
3237 });
3238
3239 // Bind a tuple wrapper's carried `$var`/`!label` keys into scope;
3240 // returns the saved prior values so they can be restored.
3241 let bind_tuple = |ev: &mut Self,
3242 tuple: &IndexMap<String, JValue>|
3243 -> Vec<(String, Option<JValue>)> {
3244 let mut saved = Vec::new();
3245 for (k, v) in tuple.iter() {
3246 let name = if let Some(n) = k.strip_prefix('$') {
3247 if n.is_empty() {
3248 continue;
3249 } else {
3250 n.to_string()
3251 }
3252 } else if k.starts_with('!') {
3253 k.clone()
3254 } else {
3255 continue;
3256 };
3257 saved.push((name.clone(), ev.context.lookup(&name).cloned()));
3258 ev.context.bind(name, v.clone());
3259 }
3260 saved
3261 };
3262 let restore = |ev: &mut Self, saved: Vec<(String, Option<JValue>)>| {
3263 for (name, old) in saved.into_iter().rev() {
3264 match old {
3265 Some(v) => ev.context.bind(name, v),
3266 None => ev.context.unbind(&name),
3267 }
3268 }
3269 };
3270
3271 // Phase 1: Group items by key expression
3272 // groups maps key -> (grouped_data, expr_index)
3273 // When multiple items have same key, their data is appended together
3274 let mut groups: HashMap<String, (Vec<JValue>, usize)> = HashMap::new();
3275
3276 // Save the current $ binding to restore later
3277 let saved_dollar = self.context.lookup("$").cloned();
3278
3279 for item in &items {
3280 // In reduce mode evaluate the key against `@` with tuple keys
3281 // bound; otherwise against the item itself.
3282 let (key_data, tuple_saved) = match (reduce, item) {
3283 (true, JValue::Object(o)) => {
3284 let saved = bind_tuple(self, o);
3285 (
3286 o.get("@").cloned().unwrap_or(JValue::Undefined),
3287 Some(saved),
3288 )
3289 }
3290 _ => (item.clone(), None),
3291 };
3292 self.context.bind("$".to_string(), key_data.clone());
3293
3294 for (pair_index, (key_node, _value_node)) in pattern.iter().enumerate() {
3295 // Evaluate key with current item as context
3296 let key = match self.evaluate_internal(key_node, &key_data)? {
3297 JValue::String(s) => s,
3298 JValue::Null => continue, // Skip null keys
3299 other => {
3300 // Skip undefined keys
3301 if other.is_undefined() {
3302 continue;
3303 }
3304 if let Some(saved) = tuple_saved {
3305 restore(self, saved);
3306 }
3307 return Err(EvaluatorError::TypeError(format!(
3308 "T1003: Object key must be a string, got: {:?}",
3309 other
3310 )));
3311 }
3312 };
3313
3314 // Group items by key
3315 if let Some((existing_data, existing_idx)) = groups.get_mut(&*key) {
3316 // Key already exists - check if from same expression index
3317 if *existing_idx != pair_index {
3318 if let Some(saved) = tuple_saved {
3319 restore(self, saved);
3320 }
3321 // D1009: multiple key expressions evaluate to same key
3322 return Err(EvaluatorError::EvaluationError(format!(
3323 "D1009: Multiple key expressions evaluate to same key: {}",
3324 key
3325 )));
3326 }
3327 // Append item to the group
3328 existing_data.push(item.clone());
3329 } else {
3330 // New key - create new group
3331 groups.insert(key.to_string(), (vec![item.clone()], pair_index));
3332 }
3333 }
3334
3335 if let Some(saved) = tuple_saved {
3336 restore(self, saved);
3337 }
3338 }
3339
3340 // Phase 2: Evaluate value expression for each group
3341 let mut result = IndexMap::new();
3342
3343 for (key, (grouped_data, expr_index)) in groups {
3344 // Get the value expression for this group
3345 let (_key_node, value_node) = &pattern[expr_index];
3346
3347 if reduce {
3348 // Reduce the grouped tuples into one (per-key values
3349 // appended), mirroring jsonata-js reduceTupleStream, then
3350 // evaluate the value against the merged `@` with the merged
3351 // focus/index/ancestor keys bound.
3352 let merged = reduce_tuple_stream(&grouped_data);
3353 let context = merged.get("@").cloned().unwrap_or(JValue::Undefined);
3354 let mut tuple_no_at = merged.clone();
3355 tuple_no_at.shift_remove("@");
3356 let saved = bind_tuple(self, &tuple_no_at);
3357 self.context.bind("$".to_string(), context.clone());
3358 let value = self.evaluate_internal(value_node, &context);
3359 restore(self, saved);
3360 let value = value?;
3361 if !value.is_undefined() {
3362 result.insert(key, value);
3363 }
3364 continue;
3365 }
3366
3367 // Determine the context for value evaluation:
3368 // - If single item, use that item directly
3369 // - If multiple items, use the array of items
3370 let context = if grouped_data.len() == 1 {
3371 grouped_data.into_iter().next().unwrap()
3372 } else {
3373 JValue::array(grouped_data)
3374 };
3375
3376 // Bind $ to the context for value evaluation
3377 self.context.bind("$".to_string(), context.clone());
3378
3379 // Evaluate value expression with grouped context
3380 let value = self.evaluate_internal(value_node, &context)?;
3381
3382 // Skip undefined values
3383 if !value.is_undefined() {
3384 result.insert(key, value);
3385 }
3386 }
3387
3388 // Restore the previous $ binding
3389 if let Some(saved) = saved_dollar {
3390 self.context.bind("$".to_string(), saved);
3391 } else {
3392 self.context.unbind("$");
3393 }
3394
3395 Ok(JValue::object(result))
3396 }
3397
3398 AstNode::Function {
3399 name,
3400 args,
3401 is_builtin,
3402 } => self.evaluate_function_call(name, args, *is_builtin, data),
3403
3404 // Call: invoke an arbitrary expression as a function
3405 // Used for IIFE patterns like (function($x){...})(5) or chained calls
3406 AstNode::Call { procedure, args } => {
3407 // Evaluate the procedure to get the callable value
3408 let callable = self.evaluate_internal(procedure, data)?;
3409
3410 // Check if it's a lambda value
3411 if let Some(stored_lambda) = self.lookup_lambda_from_value(&callable) {
3412 let mut evaluated_args = Vec::with_capacity(args.len());
3413 for arg in args.iter() {
3414 evaluated_args.push(self.evaluate_internal(arg, data)?);
3415 }
3416 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
3417 }
3418
3419 // Not a callable value
3420 Err(EvaluatorError::TypeError(format!(
3421 "Cannot call non-function value: {:?}",
3422 callable
3423 )))
3424 }
3425
3426 AstNode::Conditional {
3427 condition,
3428 then_branch,
3429 else_branch,
3430 } => {
3431 let condition_value = self.evaluate_internal(condition, data)?;
3432 if self.is_truthy(&condition_value) {
3433 self.evaluate_internal(then_branch, data)
3434 } else if let Some(else_branch) = else_branch {
3435 self.evaluate_internal(else_branch, data)
3436 } else {
3437 // No else branch - return undefined (not null)
3438 // This allows $map to filter out results from conditionals without else
3439 Ok(JValue::Undefined)
3440 }
3441 }
3442
3443 AstNode::Block(expressions) => {
3444 // Blocks create a new scope - push scope instead of clone/restore
3445 self.context.push_scope();
3446
3447 let mut result = JValue::Null;
3448 for expr in expressions {
3449 result = self.evaluate_internal(expr, data)?;
3450 }
3451
3452 // Before popping, preserve any lambdas referenced by the result
3453 // This is essential for closures returned from blocks (IIFE pattern)
3454 let lambdas_to_keep = self.extract_lambda_ids(&result);
3455 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
3456
3457 Ok(result)
3458 }
3459
3460 // Lambda: capture current environment for closure support
3461 AstNode::Lambda {
3462 params,
3463 body,
3464 signature,
3465 thunk,
3466 } => {
3467 let lambda_id = format!("__lambda_{}_{}", params.len(), self.fresh_lambda_id());
3468
3469 let compiled_body = if !thunk {
3470 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
3471 try_compile_expr_with_allowed_vars(body, &var_refs)
3472 } else {
3473 None
3474 };
3475 let stored_lambda = StoredLambda {
3476 params: params.clone(),
3477 body: (**body).clone(),
3478 compiled_body,
3479 signature: signature.clone(),
3480 captured_env: self.capture_environment_for(body, params),
3481 captured_data: Some(data.clone()),
3482 thunk: *thunk,
3483 };
3484 self.context.bind_lambda(lambda_id.clone(), stored_lambda);
3485
3486 let lambda_obj = JValue::lambda(
3487 lambda_id.as_str(),
3488 params.clone(),
3489 None::<String>,
3490 signature.clone(),
3491 );
3492
3493 Ok(lambda_obj)
3494 }
3495
3496 // Wildcard: collect all values from current object
3497 AstNode::Wildcard => {
3498 match data {
3499 JValue::Object(obj) => {
3500 let mut result = Vec::new();
3501 for value in obj.values() {
3502 // Flatten arrays into the result
3503 match value {
3504 JValue::Array(arr) => result.extend(arr.iter().cloned()),
3505 _ => result.push(value.clone()),
3506 }
3507 }
3508 Ok(JValue::array(result))
3509 }
3510 JValue::Array(arr) => {
3511 // For arrays, wildcard returns all elements
3512 Ok(JValue::Array(arr.clone()))
3513 }
3514 _ => Ok(JValue::Null),
3515 }
3516 }
3517
3518 // Descendant: recursively traverse all nested values
3519 AstNode::Descendant => {
3520 let descendants = self.collect_descendants(data);
3521 if descendants.is_empty() {
3522 Ok(JValue::Null) // No descendants means undefined
3523 } else {
3524 Ok(JValue::array(descendants))
3525 }
3526 }
3527
3528 AstNode::Predicate(_) => Err(EvaluatorError::EvaluationError(
3529 "Predicate can only be used in path expressions".to_string(),
3530 )),
3531
3532 // Array grouping: same as Array but prevents flattening in path contexts
3533 AstNode::ArrayGroup(elements) => {
3534 let mut result = Vec::new();
3535 for element in elements {
3536 let value = self.evaluate_internal(element, data)?;
3537 result.push(value);
3538 }
3539 Ok(JValue::array(result))
3540 }
3541
3542 AstNode::FunctionApplication(_) => Err(EvaluatorError::EvaluationError(
3543 "Function application can only be used in path expressions".to_string(),
3544 )),
3545
3546 AstNode::Sort { input, terms } => {
3547 // Keep the input path's tuple wrappers so the sort terms can read
3548 // the carried `%`/`$focus`/`$index` bindings per element.
3549 let saved = self.keep_tuple_stream;
3550 self.keep_tuple_stream = true;
3551 let value = self.evaluate_internal(input, data);
3552 self.keep_tuple_stream = saved;
3553 self.evaluate_sort(&value?, terms)
3554 }
3555
3556 // Transform: |location|update[,delete]|
3557 AstNode::Transform {
3558 location,
3559 update,
3560 delete,
3561 } => {
3562 // Check if $ is bound (meaning we're being invoked as a lambda)
3563 if self.context.lookup("$").is_some() {
3564 // Execute the transformation
3565 self.execute_transform(location, update, delete.as_deref(), data)
3566 } else {
3567 // Return a lambda representation
3568 // The transform will be executed when the lambda is invoked
3569 let transform_lambda = StoredLambda {
3570 params: vec!["$".to_string()],
3571 body: AstNode::Transform {
3572 location: location.clone(),
3573 update: update.clone(),
3574 delete: delete.clone(),
3575 },
3576 compiled_body: None, // Transform is not a pure compilable expr
3577 signature: None,
3578 captured_env: HashMap::new(),
3579 captured_data: None, // Transform takes $ as parameter
3580 thunk: false,
3581 };
3582
3583 // Store with a generated unique name
3584 let lambda_name = format!("__transform_{}", self.fresh_lambda_id());
3585 self.context.bind_lambda(lambda_name, transform_lambda);
3586
3587 // Return lambda marker
3588 Ok(JValue::string("<lambda>"))
3589 }
3590 }
3591
3592 // Parent-reference operator (%): ast_transform has already resolved
3593 // this to a synthetic ancestor label ("!0", "!1", ...). The enclosing
3594 // tuple step binds that label into scope (create_tuple_stream +
3595 // needs_tuple_context_binding), so resolving it is an ordinary scope
3596 // lookup, mirroring jsonata-js's
3597 // `case 'parent': result = environment.lookup(expr.slot.label);`.
3598 AstNode::Parent(label) => {
3599 if let Some(v) = self.context.lookup(label) {
3600 return Ok(v.clone());
3601 }
3602 // Fall back to the tuple wrapper carried as `data`: a `%` used
3603 // inside a predicate/stage over a tuple stream -- e.g.
3604 // `(Account.Order.Product)[%.OrderID='order104'].SKU`, where the
3605 // predicate is evaluated per tuple with the wrapper as data --
3606 // reads its ancestor from the tuple's `!label` key, which isn't
3607 // separately bound into scope here (mirrors AstNode::Variable's
3608 // tuple-binding fallback below).
3609 if let JValue::Object(obj) = data {
3610 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3611 if let Some(v) = obj.get(label) {
3612 return Ok(v.clone());
3613 }
3614 }
3615 }
3616 Ok(JValue::Undefined)
3617 }
3618 }
3619 }
3620
3621 /// Apply stages (filters/predicates) to a value during field extraction
3622 /// Non-array values are wrapped in an array before filtering (JSONata semantics)
3623 /// This matches the JavaScript reference where stages apply to sequences
3624 fn apply_stages(&mut self, value: JValue, stages: &[Stage]) -> Result<JValue, EvaluatorError> {
3625 // Wrap non-arrays in an array for filtering (JSONata semantics)
3626 let mut result = match value {
3627 JValue::Null => return Ok(JValue::Null), // Null passes through unchanged
3628 JValue::Array(_) => value,
3629 other => JValue::array(vec![other]),
3630 };
3631
3632 for stage in stages {
3633 match stage {
3634 Stage::Filter(predicate_expr) => {
3635 // When applying stages, use stage-specific predicate logic
3636 result = self.evaluate_predicate_as_stage(&result, predicate_expr)?;
3637 }
3638 // Positional index stages are meaningful only over a tuple stream
3639 // (they set a variable to each tuple's position); they are applied
3640 // in `create_tuple_stream`, not on a plain value sequence here.
3641 Stage::Index(_) => {}
3642 }
3643 }
3644 Ok(result)
3645 }
3646
3647 /// Check if an AST node is definitely a filter expression (comparison/logical)
3648 /// rather than a potential numeric index. When true, we skip speculative numeric evaluation.
3649 fn is_filter_predicate(predicate: &AstNode) -> bool {
3650 match predicate {
3651 AstNode::Binary { op, .. } => matches!(
3652 op,
3653 BinaryOp::GreaterThan
3654 | BinaryOp::GreaterThanOrEqual
3655 | BinaryOp::LessThan
3656 | BinaryOp::LessThanOrEqual
3657 | BinaryOp::Equal
3658 | BinaryOp::NotEqual
3659 | BinaryOp::And
3660 | BinaryOp::Or
3661 | BinaryOp::In
3662 ),
3663 AstNode::Unary {
3664 op: crate::ast::UnaryOp::Not,
3665 ..
3666 } => true,
3667 _ => false,
3668 }
3669 }
3670
3671 /// Evaluate a predicate as a stage during field extraction
3672 /// This has different semantics than standalone predicates:
3673 /// - Maps index operations over arrays of extracted values
3674 fn evaluate_predicate_as_stage(
3675 &mut self,
3676 current: &JValue,
3677 predicate: &AstNode,
3678 ) -> Result<JValue, EvaluatorError> {
3679 // Special case: empty brackets [] (represented as Boolean(true))
3680 if matches!(predicate, AstNode::Boolean(true)) {
3681 return match current {
3682 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
3683 JValue::Null => Ok(JValue::Null),
3684 other => Ok(JValue::array(vec![other.clone()])),
3685 };
3686 }
3687
3688 match current {
3689 JValue::Array(arr) => {
3690 // For stages: if we have an array of values (from field extraction),
3691 // apply the predicate to each value if appropriate
3692
3693 // Check if predicate is a numeric index
3694 if let AstNode::Number(n) = predicate {
3695 // Check if this is an array of arrays (extracted array fields)
3696 let is_array_of_arrays =
3697 arr.iter().any(|item| matches!(item, JValue::Array(_)));
3698
3699 if !is_array_of_arrays {
3700 // Simple values: just index normally
3701 return self.array_index(current, &JValue::Number(*n));
3702 }
3703
3704 // Array of arrays: map index access over each extracted array
3705 let mut result = Vec::new();
3706 for item in arr.iter() {
3707 match item {
3708 JValue::Array(_) => {
3709 let indexed = self.array_index(item, &JValue::Number(*n))?;
3710 if !indexed.is_null() && !indexed.is_undefined() {
3711 result.push(indexed);
3712 }
3713 }
3714 _ => {
3715 if *n == 0.0 {
3716 result.push(item.clone());
3717 }
3718 }
3719 }
3720 }
3721 return Ok(JValue::array(result));
3722 }
3723
3724 // Short-circuit: if predicate is definitely a comparison/logical expression,
3725 // skip speculative numeric evaluation and go directly to filter logic
3726 if Self::is_filter_predicate(predicate) {
3727 // Try CompiledExpr fast path (handles compound predicates, arithmetic, etc.)
3728 if let Some(compiled) = try_compile_expr(predicate) {
3729 let shape = arr.first().and_then(build_shape_cache);
3730 let mut filtered = Vec::with_capacity(arr.len());
3731 for item in arr.iter() {
3732 let result = if let Some(ref s) = shape {
3733 eval_compiled_shaped(&compiled, item, None, s)?
3734 } else {
3735 eval_compiled(&compiled, item, None)?
3736 };
3737 if compiled_is_truthy(&result) {
3738 filtered.push(item.clone());
3739 }
3740 }
3741 return Ok(JValue::array(filtered));
3742 }
3743 // Fallback: full AST evaluation
3744 let mut filtered = Vec::new();
3745 for item in arr.iter() {
3746 let item_result = self.evaluate_internal(predicate, item)?;
3747 if self.is_truthy(&item_result) {
3748 filtered.push(item.clone());
3749 }
3750 }
3751 return Ok(JValue::array(filtered));
3752 }
3753
3754 // Try to evaluate the predicate to see if it's a numeric index or array of indices
3755 // If evaluation succeeds and yields a number, use it as an index
3756 // If it yields an array of numbers, use them as multiple indices
3757 // If evaluation fails (e.g., comparison error), treat as filter
3758 match self.evaluate_internal(predicate, current) {
3759 Ok(JValue::Number(n)) => {
3760 let n_val = n;
3761 let is_array_of_arrays =
3762 arr.iter().any(|item| matches!(item, JValue::Array(_)));
3763
3764 if !is_array_of_arrays {
3765 let pred_result = JValue::Number(n_val);
3766 return self.array_index(current, &pred_result);
3767 }
3768
3769 // Array of arrays: map index access
3770 let mut result = Vec::new();
3771 let pred_result = JValue::Number(n_val);
3772 for item in arr.iter() {
3773 match item {
3774 JValue::Array(_) => {
3775 let indexed = self.array_index(item, &pred_result)?;
3776 if !indexed.is_null() && !indexed.is_undefined() {
3777 result.push(indexed);
3778 }
3779 }
3780 _ => {
3781 if n_val == 0.0 {
3782 result.push(item.clone());
3783 }
3784 }
3785 }
3786 }
3787 return Ok(JValue::array(result));
3788 }
3789 Ok(JValue::Array(indices)) => {
3790 // Array of values - could be indices or filter results
3791 // Check if all values are numeric
3792 let has_non_numeric =
3793 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
3794
3795 if has_non_numeric {
3796 // Non-numeric values - treat as filter, fall through
3797 } else {
3798 // All numeric - use as indices
3799 let arr_len = arr.len() as i64;
3800 let mut resolved_indices: Vec<i64> = indices
3801 .iter()
3802 .filter_map(|v| {
3803 if let JValue::Number(n) = v {
3804 let idx = *n as i64;
3805 // Resolve negative indices
3806 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
3807 // Only include valid indices
3808 if actual_idx >= 0 && actual_idx < arr_len {
3809 Some(actual_idx)
3810 } else {
3811 None
3812 }
3813 } else {
3814 None
3815 }
3816 })
3817 .collect();
3818
3819 // Sort and deduplicate indices
3820 resolved_indices.sort();
3821 resolved_indices.dedup();
3822
3823 // Select elements at each sorted index
3824 let result: Vec<JValue> = resolved_indices
3825 .iter()
3826 .map(|&idx| arr[idx as usize].clone())
3827 .collect();
3828
3829 return Ok(JValue::array(result));
3830 }
3831 }
3832 Ok(_) => {
3833 // Evaluated successfully but not a number or array - might be a filter
3834 // Fall through to filter logic
3835 }
3836 Err(_) => {
3837 // Evaluation failed - it's likely a filter expression
3838 // Fall through to filter logic
3839 }
3840 }
3841
3842 // It's a filter expression
3843 let mut filtered = Vec::new();
3844 for item in arr.iter() {
3845 let item_result = self.evaluate_internal(predicate, item)?;
3846 if self.is_truthy(&item_result) {
3847 filtered.push(item.clone());
3848 }
3849 }
3850 Ok(JValue::array(filtered))
3851 }
3852 JValue::Null => {
3853 // Null: return null
3854 Ok(JValue::Null)
3855 }
3856 other => {
3857 // Non-array values: treat as single-element conceptual array
3858 // For numeric predicates: index 0 returns the value, other indices return null
3859 // For boolean predicates: if truthy, return value; if falsy, return null
3860
3861 // Check if predicate is a numeric index
3862 if let AstNode::Number(n) = predicate {
3863 // Index 0 returns the value, other indices return null
3864 if *n == 0.0 {
3865 return Ok(other.clone());
3866 } else {
3867 return Ok(JValue::Null);
3868 }
3869 }
3870
3871 // Try to evaluate the predicate to see if it's a numeric index
3872 match self.evaluate_internal(predicate, other) {
3873 Ok(JValue::Number(n)) => {
3874 // Index 0 returns the value, other indices return null
3875 if n == 0.0 {
3876 Ok(other.clone())
3877 } else {
3878 Ok(JValue::Null)
3879 }
3880 }
3881 Ok(pred_result) => {
3882 // Boolean filter: return value if truthy, null if falsy
3883 if self.is_truthy(&pred_result) {
3884 Ok(other.clone())
3885 } else {
3886 Ok(JValue::Null)
3887 }
3888 }
3889 Err(e) => Err(e),
3890 }
3891 }
3892 }
3893 }
3894
3895 /// Evaluate a path expression (e.g., foo.bar.baz)
3896 fn evaluate_path(
3897 &mut self,
3898 steps: &[PathStep],
3899 data: &JValue,
3900 ) -> Result<JValue, EvaluatorError> {
3901 // Avoid cloning by using references and only cloning when necessary
3902 if steps.is_empty() {
3903 return Ok(data.clone());
3904 }
3905
3906 // Fast path: single field access on object
3907 // This is a very common pattern, so optimize it.
3908 // Skipped for tuple-binding steps (@/#/%), which need full tuple-stream
3909 // creation handled below.
3910 if steps.len() == 1 && !Self::step_creates_tuple(&steps[0]) {
3911 if let AstNode::Name(field_name) = &steps[0].node {
3912 return match data {
3913 JValue::Object(obj) => {
3914 // Check if this is a tuple - extract '@' value
3915 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
3916 if let Some(JValue::Object(inner)) = obj.get("@") {
3917 Ok(inner.get(field_name).cloned().unwrap_or(JValue::Undefined))
3918 } else {
3919 Ok(JValue::Undefined)
3920 }
3921 } else {
3922 Ok(obj.get(field_name).cloned().unwrap_or(JValue::Undefined))
3923 }
3924 }
3925 JValue::Array(arr) => {
3926 // Array mapping: extract field from each element
3927 // Optimized: use references to access fields without cloning entire objects
3928 // Check first element for tuple-ness (tuples are all-or-nothing)
3929 let has_tuples = arr.first().is_some_and(|item| {
3930 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
3931 });
3932
3933 if !has_tuples {
3934 // Fast path: no tuples, just direct field lookups
3935 let mut result = Vec::with_capacity(arr.len());
3936 for item in arr.iter() {
3937 if let JValue::Object(obj) = item {
3938 if let Some(val) = obj.get(field_name) {
3939 if !val.is_null() {
3940 match val {
3941 JValue::Array(arr_val) => {
3942 result.extend(arr_val.iter().cloned());
3943 }
3944 other => result.push(other.clone()),
3945 }
3946 }
3947 }
3948 } else if let JValue::Array(inner_arr) = item {
3949 let nested_result = self.evaluate_path(
3950 &[PathStep::new(AstNode::Name(field_name.clone()))],
3951 &JValue::Array(inner_arr.clone()),
3952 )?;
3953 match nested_result {
3954 JValue::Array(nested) => {
3955 result.extend(nested.iter().cloned());
3956 }
3957 JValue::Null => {}
3958 other => result.push(other),
3959 }
3960 }
3961 }
3962
3963 if result.is_empty() {
3964 Ok(JValue::Null)
3965 } else if result.len() == 1 {
3966 Ok(result.into_iter().next().unwrap())
3967 } else {
3968 Ok(JValue::array(result))
3969 }
3970 } else {
3971 // Tuple path: per-element tuple handling
3972 let mut result = Vec::new();
3973 for item in arr.iter() {
3974 match item {
3975 JValue::Object(obj) => {
3976 let is_tuple =
3977 obj.get("__tuple__") == Some(&JValue::Bool(true));
3978
3979 if is_tuple {
3980 let inner = match obj.get("@") {
3981 Some(JValue::Object(inner)) => inner,
3982 _ => continue,
3983 };
3984
3985 if let Some(val) = inner.get(field_name) {
3986 if !val.is_null() {
3987 // Build tuple wrapper - only clone bindings when needed
3988 let wrap = |v: JValue| -> JValue {
3989 let mut wrapper = IndexMap::new();
3990 wrapper.insert("@".to_string(), v);
3991 wrapper.insert(
3992 "__tuple__".to_string(),
3993 JValue::Bool(true),
3994 );
3995 for (k, v) in obj.iter() {
3996 if k.starts_with('$') {
3997 wrapper
3998 .insert(k.clone(), v.clone());
3999 }
4000 }
4001 JValue::object(wrapper)
4002 };
4003
4004 match val {
4005 JValue::Array(arr_val) => {
4006 for item in arr_val.iter() {
4007 result.push(wrap(item.clone()));
4008 }
4009 }
4010 other => result.push(wrap(other.clone())),
4011 }
4012 }
4013 }
4014 } else {
4015 // Non-tuple: access field directly by reference, only clone the field value
4016 if let Some(val) = obj.get(field_name) {
4017 if !val.is_null() {
4018 match val {
4019 JValue::Array(arr_val) => {
4020 for item in arr_val.iter() {
4021 result.push(item.clone());
4022 }
4023 }
4024 other => result.push(other.clone()),
4025 }
4026 }
4027 }
4028 }
4029 }
4030 JValue::Array(inner_arr) => {
4031 // Recursively map over nested array
4032 let nested_result = self.evaluate_path(
4033 &[PathStep::new(AstNode::Name(field_name.clone()))],
4034 &JValue::Array(inner_arr.clone()),
4035 )?;
4036 // Add nested result to our results
4037 match nested_result {
4038 JValue::Array(nested) => {
4039 // Flatten nested arrays from recursive mapping
4040 result.extend(nested.iter().cloned());
4041 }
4042 JValue::Null => {} // Skip nulls from nested arrays
4043 other => result.push(other),
4044 }
4045 }
4046 _ => {} // Skip non-object items
4047 }
4048 }
4049
4050 // Return array result
4051 // JSONata singleton unwrapping: if we have exactly one result,
4052 // unwrap it (even if it's an array)
4053 if result.is_empty() {
4054 Ok(JValue::Null)
4055 } else if result.len() == 1 {
4056 Ok(result.into_iter().next().unwrap())
4057 } else {
4058 Ok(JValue::array(result))
4059 }
4060 } // end else (tuple path)
4061 }
4062 _ => Ok(JValue::Undefined),
4063 };
4064 }
4065 }
4066
4067 // Fast path: 2-step $variable.field with no stages
4068 // Handles common patterns like $l.rating, $item.price in sort/HOF bodies
4069 if steps.len() == 2 && steps[0].stages.is_empty() && steps[1].stages.is_empty() {
4070 if let (AstNode::Variable(var_name), AstNode::Name(field_name)) =
4071 (&steps[0].node, &steps[1].node)
4072 {
4073 if !var_name.is_empty() {
4074 if let Some(value) = self.context.lookup(var_name) {
4075 match value {
4076 JValue::Object(obj) => {
4077 return Ok(obj.get(field_name).cloned().unwrap_or(JValue::Null));
4078 }
4079 JValue::Array(arr) => {
4080 // Map field extraction over array (same as single-step Name on Array)
4081 let mut result = Vec::with_capacity(arr.len());
4082 for item in arr.iter() {
4083 if let JValue::Object(obj) = item {
4084 if let Some(val) = obj.get(field_name) {
4085 if !val.is_null() {
4086 match val {
4087 JValue::Array(inner) => {
4088 result.extend(inner.iter().cloned());
4089 }
4090 other => result.push(other.clone()),
4091 }
4092 }
4093 }
4094 }
4095 }
4096 return match result.len() {
4097 0 => Ok(JValue::Null),
4098 1 => Ok(result.pop().unwrap()),
4099 _ => Ok(JValue::array(result)),
4100 };
4101 }
4102 _ => {} // Fall through to general path evaluation
4103 }
4104 }
4105 }
4106 }
4107 }
4108
4109 // Track whether we did array mapping (for singleton unwrapping)
4110 let mut did_array_mapping = false;
4111
4112 // For the first step, work with a reference.
4113 // Tuple-binding first steps (e.g. `items#$i`, `foo@$v`) create a tuple
4114 // stream up front, mirroring jsonata-js's evaluateTupleStep for the
4115 // first path step where tupleBindings is undefined.
4116 let mut current: JValue = if Self::step_creates_tuple(&steps[0]) {
4117 JValue::array(self.create_tuple_stream(&steps[0], data, true)?)
4118 } else {
4119 match &steps[0].node {
4120 AstNode::Wildcard => {
4121 // Wildcard as first step
4122 match data {
4123 JValue::Object(obj) => {
4124 let mut result = Vec::new();
4125 for value in obj.values() {
4126 // Flatten arrays into the result
4127 match value {
4128 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4129 _ => result.push(value.clone()),
4130 }
4131 }
4132 JValue::array(result)
4133 }
4134 JValue::Array(arr) => JValue::Array(arr.clone()),
4135 _ => JValue::Null,
4136 }
4137 }
4138 AstNode::Descendant => {
4139 // Descendant as first step
4140 let descendants = self.collect_descendants(data);
4141 JValue::array(descendants)
4142 }
4143 AstNode::ParentVariable(name) => {
4144 // Parent variable as first step
4145 let parent_data = self.context.get_parent().ok_or_else(|| {
4146 EvaluatorError::ReferenceError("Parent context not available".to_string())
4147 })?;
4148
4149 if name.is_empty() {
4150 // $$ alone returns parent context
4151 parent_data.clone()
4152 } else {
4153 // $$field accesses field on parent
4154 match parent_data {
4155 JValue::Object(obj) => obj.get(name).cloned().unwrap_or(JValue::Null),
4156 _ => JValue::Null,
4157 }
4158 }
4159 }
4160 AstNode::Name(field_name) => {
4161 // Field/property access - get the stages for this step
4162 let stages = &steps[0].stages;
4163
4164 match data {
4165 JValue::Object(obj) => {
4166 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4167 // Apply any stages to the extracted value
4168 if !stages.is_empty() {
4169 self.apply_stages(val, stages)?
4170 } else {
4171 val
4172 }
4173 }
4174 JValue::Array(arr) => {
4175 // Array mapping: extract field from each element and apply stages
4176 let mut result = Vec::new();
4177 for item in arr.iter() {
4178 match item {
4179 JValue::Object(obj) => {
4180 let val = obj
4181 .get(field_name)
4182 .cloned()
4183 .unwrap_or(JValue::Undefined);
4184 if !val.is_null() && !val.is_undefined() {
4185 if !stages.is_empty() {
4186 // Apply stages to the extracted value
4187 let processed_val =
4188 self.apply_stages(val, stages)?;
4189 // Stages always return an array (or null); extend results
4190 match processed_val {
4191 JValue::Array(arr) => {
4192 result.extend(arr.iter().cloned())
4193 }
4194 JValue::Null => {} // Skip nulls from stage application
4195 other => result.push(other), // Shouldn't happen, but handle it
4196 }
4197 } else {
4198 // No stages: flatten arrays, push scalars
4199 match val {
4200 JValue::Array(arr) => {
4201 result.extend(arr.iter().cloned())
4202 }
4203 other => result.push(other),
4204 }
4205 }
4206 }
4207 }
4208 JValue::Array(inner_arr) => {
4209 // Recursively map over nested array
4210 let nested_result = self.evaluate_path(
4211 &[steps[0].clone()],
4212 &JValue::Array(inner_arr.clone()),
4213 )?;
4214 match nested_result {
4215 JValue::Array(nested) => {
4216 result.extend(nested.iter().cloned())
4217 }
4218 JValue::Null => {} // Skip nulls from nested arrays
4219 other => result.push(other),
4220 }
4221 }
4222 _ => {} // Skip non-object items
4223 }
4224 }
4225 JValue::array(result)
4226 }
4227 JValue::Null => JValue::Null,
4228 // Accessing field on non-object returns undefined (not an error)
4229 _ => JValue::Undefined,
4230 }
4231 }
4232 AstNode::String(string_literal) => {
4233 // String literal in path context - evaluate as literal and apply stages
4234 // This handles cases like "Red"[true] where "Red" is a literal, not a field access
4235 let stages = &steps[0].stages;
4236 let val = JValue::string(string_literal.clone());
4237
4238 if !stages.is_empty() {
4239 // Apply stages (predicates) to the string literal
4240 let result = self.apply_stages(val, stages)?;
4241 // Unwrap single-element arrays back to scalar
4242 // (string literals with predicates should return scalar or null, not arrays)
4243 match result {
4244 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4245 JValue::Array(arr) if arr.is_empty() => JValue::Null,
4246 other => other,
4247 }
4248 } else {
4249 val
4250 }
4251 }
4252 AstNode::Predicate(pred_expr) => {
4253 // Predicate as first step
4254 self.evaluate_predicate(data, pred_expr)?
4255 }
4256 _ => {
4257 // Complex first step - evaluate it. When the step is
4258 // tuple-carrying (e.g. a parenthesized `(Account.Order.Product)`
4259 // whose `Product` is `%`-tagged, as in
4260 // `(Account.Order.Product)[%.OrderID='order104'].SKU`), keep the
4261 // inner path's tuple wrappers so the following predicate/step
4262 // can read the `!label` bindings.
4263 let saved_keep = self.keep_tuple_stream;
4264 if steps[0].is_tuple {
4265 self.keep_tuple_stream = true;
4266 }
4267 let v = self.evaluate_path_step(&steps[0].node, data, data);
4268 self.keep_tuple_stream = saved_keep;
4269 v?
4270 }
4271 }
4272 };
4273
4274 // Process remaining steps
4275 for (step_idx, step) in steps[1..].iter().enumerate() {
4276 let is_last_step = step_idx == steps.len() - 2;
4277 // Early return if current is null/undefined - no point continuing
4278 // This handles cases like `blah.{}` where blah doesn't exist
4279 if current.is_null() {
4280 return Ok(JValue::Null);
4281 }
4282 if current.is_undefined() {
4283 return Ok(JValue::Undefined);
4284 }
4285
4286 // A lone tuple wrapper (e.g. from a numeric index predicate `[1]` over
4287 // a tuple stream, which selects a single tuple and unwraps it out of
4288 // the array) must stay a tuple stream so the following step keeps
4289 // reading its carried `$focus`/`!label` bindings. Re-wrap it as a
4290 // one-element array (e.g. `library.loans@$l.books@$b[...][1].{...}`).
4291 if let JValue::Object(o) = ¤t {
4292 if o.get("__tuple__") == Some(&JValue::Bool(true)) {
4293 current = JValue::array(vec![current.clone()]);
4294 // The lone wrapper came from a singleton index selection, so
4295 // the final result should unwrap back to a scalar (a following
4296 // object step must not leave a spurious 1-element array).
4297 did_array_mapping = true;
4298 }
4299 }
4300
4301 // Check if current is a tuple array - if so, we need to bind tuple variables
4302 // to context so they're available in nested expressions (like predicates)
4303 let is_tuple_array = if let JValue::Array(arr) = ¤t {
4304 arr.first().is_some_and(|first| {
4305 if let JValue::Object(obj) = first {
4306 obj.get("__tuple__") == Some(&JValue::Bool(true))
4307 } else {
4308 false
4309 }
4310 })
4311 } else {
4312 false
4313 };
4314
4315 // Tuple-binding step (@ focus / # index / % parent): create/extend the
4316 // tuple stream, mirroring jsonata-js's evaluateTupleStep. Downstream
4317 // (non-binding) steps then consume the {@, $var, !label, __tuple__}
4318 // wrappers via the existing tuple-aware handling below.
4319 //
4320 // A `%` reference used AS a path step (`AstNode::Parent`, e.g. the
4321 // `.%` in `Account.Order.Product.Price.%[...]`) must also extend the
4322 // stream, but ONLY when it is consuming an existing tuple stream:
4323 // its ancestor label lives in those incoming tuples, so
4324 // create_tuple_stream's per-tuple frame binding is what lets
4325 // `evaluate_internal(Parent, ..)` resolve it (and any predicate
4326 // stage on the `%` step then resolves in the same frame). A `%`
4327 // that instead LEADS a fresh path (e.g. the `%.OrderID` inside a
4328 // predicate, whose input is plain data, not a tuple stream) must
4329 // NOT be routed here -- it's an ordinary scope lookup.
4330 let is_parent_step_over_tuple =
4331 matches!(step.node, AstNode::Parent(_)) && is_tuple_array;
4332 if Self::step_creates_tuple(step) || is_parent_step_over_tuple {
4333 current = JValue::array(self.create_tuple_stream(step, ¤t, false)?);
4334 continue;
4335 }
4336
4337 // For tuple arrays with certain step types, we need special handling to bind
4338 // tuple variables to context so they're available in nested expressions.
4339 // This is needed for:
4340 // - Object constructors: {"label": $$.items[$i]} needs $i in context
4341 // - Function applications: .($$.items[$i]) needs $i in context
4342 // - Variable lookups: .$i needs to find the tuple binding
4343 //
4344 // Steps like Name (field access) already have proper tuple handling in their
4345 // specific cases, so we don't intercept those here.
4346 let needs_tuple_context_binding = is_tuple_array
4347 && matches!(
4348 &step.node,
4349 AstNode::Object(_)
4350 | AstNode::FunctionApplication(_)
4351 | AstNode::Variable(_)
4352 | AstNode::ArrayGroup(_)
4353 );
4354
4355 if needs_tuple_context_binding {
4356 if let JValue::Array(arr) = ¤t {
4357 let mut results = Vec::new();
4358
4359 for tuple in arr.iter() {
4360 if let JValue::Object(tuple_obj) = tuple {
4361 // Extract tuple bindings so nested expressions can see
4362 // them: `$var` focus/index bindings (stored `$name`,
4363 // bound as `name`) AND `!label` ancestor bindings for
4364 // `%` (stored and bound under the full `!label` key).
4365 // Saves/restores rather than blindly unbinding, so a
4366 // tuple key that collides with a live outer `:=`
4367 // binding doesn't get deleted afterward.
4368 let tuple_bindings = self.bind_tuple_keys(tuple_obj);
4369
4370 // Get the actual value from the tuple (@ field)
4371 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Null);
4372
4373 // Evaluate the step
4374 let step_result = match &step.node {
4375 AstNode::Variable(_) => {
4376 // Variable lookup - check context (which now has bindings)
4377 self.evaluate_internal(&step.node, tuple)?
4378 }
4379 AstNode::Object(_) | AstNode::ArrayGroup(_) => {
4380 // Object / array constructor step (e.g.
4381 // `Product.[`Product Name`, %.OrderID]`) -
4382 // evaluate on the tuple's `@` value with the
4383 // carried `!label`/`$focus` bindings in scope
4384 // so an embedded `%` resolves.
4385 self.evaluate_internal(&step.node, &actual_data)?
4386 }
4387 AstNode::FunctionApplication(inner) => {
4388 // A parenthesized step `(expr)` consuming a tuple stream
4389 // (e.g. `Account.Order.Product.( %.OrderID )` or
4390 // `Employee@$e.(Contact)[...]`): evaluate the INNER
4391 // expression on the tuple's `@` value with `$` bound to
4392 // it, mirroring the non-tuple FunctionApplication step
4393 // handling. Routing the wrapper node itself through
4394 // evaluate_internal raises "Function application can only
4395 // be used in path expressions".
4396 let saved_dollar = self.context.lookup("$").cloned();
4397 self.context.bind("$".to_string(), actual_data.clone());
4398 // Keep tuple wrappers from the inner path alive:
4399 // when `inner` is itself a tuple-carrying path
4400 // (e.g. `(Order.Product)` whose `Product` is
4401 // `%`-tagged), its `!label` wrappers must survive
4402 // to be merged into this tuple by the rewrap below
4403 // (they feed a later `%`/`%.%`). Without this the
4404 // inner path projects to `@` and drops the labels.
4405 let saved_keep = self.keep_tuple_stream;
4406 self.keep_tuple_stream = true;
4407 let v = self.evaluate_internal(inner, &actual_data);
4408 self.keep_tuple_stream = saved_keep;
4409 match saved_dollar {
4410 Some(s) => self.context.bind("$".to_string(), s),
4411 None => self.context.unbind("$"),
4412 }
4413 v?
4414 }
4415 _ => unreachable!(), // We only match specific types above
4416 };
4417
4418 // Apply this step's own filter stages (e.g. the
4419 // `[$substring(title,0,3)='The']` on `.$[...]` in
4420 // `library.books#$pos.$[...].$pos`) while the tuple
4421 // bindings are still in scope, so the predicate can
4422 // reference them and non-matching tuples are dropped.
4423 let step_result = if step.stages.is_empty() {
4424 step_result
4425 } else {
4426 self.apply_stages(step_result, &step.stages)?
4427 };
4428
4429 // Restore previous bindings
4430 tuple_bindings.restore(self);
4431
4432 // Rewrap results as tuples carrying this incoming
4433 // tuple's focus/index/ancestor bindings, so that
4434 // DOWNSTREAM steps keep seeing them: a predicate like
4435 // `[ssn = $e.SSN]` after `Employee@$e.(Contact)`, a
4436 // later `%`/`%.%` in `Account.Order.(Product).{...}`,
4437 // or a further path step all read those bindings from
4438 // the tuple wrapper (see AstNode::Variable's tuple
4439 // fallback). Without rewrapping, the tuple chain is
4440 // severed after a parenthesized/object/variable step
4441 // and those references resolve to nothing. The
4442 // wrappers are projected back to their `@` values by
4443 // the top-level `unwrap_tuple_output` pass.
4444 let carried: Vec<(String, JValue)> = tuple_obj
4445 .iter()
4446 .filter(|(k, _)| {
4447 (k.starts_with('$') && k.len() > 1) || k.starts_with('!')
4448 })
4449 .map(|(k, v)| (k.clone(), v.clone()))
4450 .collect();
4451 let wrap = |v: JValue| -> JValue {
4452 match v {
4453 // If the step produced a nested tuple stream
4454 // (e.g. `(Product)` whose inner `Product` is
4455 // itself `%`-tagged), MERGE the inner tuple's
4456 // keys over the carried outer bindings, mirroring
4457 // jsonata-js's `res.tupleStream` branch
4458 // (`Object.assign(tuple, res[bb])`) -- do NOT
4459 // double-wrap, which would bury `@`/`!label`
4460 // one level down and break a following `%`/`%.%`.
4461 JValue::Object(inner)
4462 if inner.get("__tuple__") == Some(&JValue::Bool(true)) =>
4463 {
4464 let mut w = IndexMap::new();
4465 for (k, val) in &carried {
4466 w.insert(k.clone(), val.clone());
4467 }
4468 for (k, val) in inner.iter() {
4469 w.insert(k.clone(), val.clone());
4470 }
4471 w.insert("__tuple__".to_string(), JValue::Bool(true));
4472 JValue::object(w)
4473 }
4474 other => {
4475 let mut w = IndexMap::new();
4476 w.insert("@".to_string(), other);
4477 for (k, val) in &carried {
4478 w.insert(k.clone(), val.clone());
4479 }
4480 w.insert("__tuple__".to_string(), JValue::Bool(true));
4481 JValue::object(w)
4482 }
4483 }
4484 };
4485 if !step_result.is_null() && !step_result.is_undefined() {
4486 // Object constructors yield one value per tuple;
4487 // other steps may yield an array to splice in.
4488 if matches!(&step.node, AstNode::Object(_)) {
4489 results.push(wrap(step_result));
4490 } else if let JValue::Array(arr) = step_result {
4491 for it in arr.iter() {
4492 results.push(wrap(it.clone()));
4493 }
4494 } else {
4495 results.push(wrap(step_result));
4496 }
4497 }
4498 }
4499 }
4500
4501 current = JValue::array(results);
4502 continue; // Skip the regular step processing
4503 }
4504 }
4505
4506 current = match &step.node {
4507 AstNode::Wildcard => {
4508 // Wildcard in path
4509 let stages = &step.stages;
4510 let wildcard_result = match ¤t {
4511 JValue::Object(obj) => {
4512 let mut result = Vec::new();
4513 for value in obj.values() {
4514 // Flatten arrays into the result
4515 match value {
4516 JValue::Array(arr) => result.extend(arr.iter().cloned()),
4517 _ => result.push(value.clone()),
4518 }
4519 }
4520 JValue::array(result)
4521 }
4522 JValue::Array(arr) => {
4523 // Map wildcard over array
4524 let mut all_values = Vec::new();
4525 for item in arr.iter() {
4526 match item {
4527 JValue::Object(obj) => {
4528 for value in obj.values() {
4529 // Flatten arrays
4530 match value {
4531 JValue::Array(arr) => {
4532 all_values.extend(arr.iter().cloned())
4533 }
4534 _ => all_values.push(value.clone()),
4535 }
4536 }
4537 }
4538 JValue::Array(inner) => {
4539 all_values.extend(inner.iter().cloned());
4540 }
4541 _ => {}
4542 }
4543 }
4544 JValue::array(all_values)
4545 }
4546 _ => JValue::Null,
4547 };
4548
4549 // Apply stages (predicates) if present
4550 if !stages.is_empty() {
4551 self.apply_stages(wildcard_result, stages)?
4552 } else {
4553 wildcard_result
4554 }
4555 }
4556 AstNode::Descendant => {
4557 // Descendant in path
4558 match ¤t {
4559 JValue::Array(arr) => {
4560 // Collect descendants from all array elements
4561 let mut all_descendants = Vec::new();
4562 for item in arr.iter() {
4563 all_descendants.extend(self.collect_descendants(item));
4564 }
4565 JValue::array(all_descendants)
4566 }
4567 _ => {
4568 // Collect descendants from current value
4569 let descendants = self.collect_descendants(¤t);
4570 JValue::array(descendants)
4571 }
4572 }
4573 }
4574 AstNode::Name(field_name) => {
4575 // Navigate into object field or map over array, applying stages
4576 let stages = &step.stages;
4577
4578 match ¤t {
4579 JValue::Object(obj) => {
4580 // Single object field extraction - NOT array mapping
4581 // This resets did_array_mapping because we're extracting from
4582 // a single value, not mapping over an array. The field's value
4583 // (even if it's an array) should be preserved as-is.
4584 did_array_mapping = false;
4585 let val = obj.get(field_name).cloned().unwrap_or(JValue::Undefined);
4586 // Apply stages if present
4587 if !stages.is_empty() {
4588 self.apply_stages(val, stages)?
4589 } else {
4590 val
4591 }
4592 }
4593 JValue::Array(arr) => {
4594 // Array mapping: extract field from each element and apply stages
4595 did_array_mapping = true; // Track that we did array mapping
4596
4597 // Fast path: if no elements are tuples and no stages,
4598 // skip all tuple checking overhead (common case for products.price etc.)
4599 // Tuples are all-or-nothing (created by index binding #$i),
4600 // so checking only the first element is sufficient.
4601 let has_tuples = arr.first().is_some_and(|item| {
4602 matches!(item, JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)))
4603 });
4604
4605 if !has_tuples && stages.is_empty() {
4606 let mut result = Vec::with_capacity(arr.len());
4607 for item in arr.iter() {
4608 match item {
4609 JValue::Object(obj) => {
4610 if let Some(val) = obj.get(field_name) {
4611 if !val.is_null() {
4612 match val {
4613 JValue::Array(arr_val) => {
4614 result.extend(arr_val.iter().cloned())
4615 }
4616 other => result.push(other.clone()),
4617 }
4618 }
4619 }
4620 }
4621 JValue::Array(_) => {
4622 let nested_result =
4623 self.evaluate_path(&[step.clone()], item)?;
4624 match nested_result {
4625 JValue::Array(nested) => {
4626 result.extend(nested.iter().cloned())
4627 }
4628 JValue::Null => {}
4629 other => result.push(other),
4630 }
4631 }
4632 _ => {}
4633 }
4634 }
4635 JValue::array(result)
4636 } else {
4637 // Full path with tuple support and stages
4638 let mut result = Vec::new();
4639
4640 for item in arr.iter() {
4641 match item {
4642 JValue::Object(obj) => {
4643 // Check if this is a tuple stream element
4644 let (actual_obj, tuple_bindings) = if obj
4645 .get("__tuple__")
4646 == Some(&JValue::Bool(true))
4647 {
4648 // This is a tuple - extract '@' value and preserve bindings
4649 if let Some(JValue::Object(inner)) = obj.get("@") {
4650 // Collect index bindings (variables starting with $)
4651 let bindings: Vec<(String, JValue)> = obj
4652 .iter()
4653 .filter(|(k, _)| k.starts_with('$'))
4654 .map(|(k, v)| (k.clone(), v.clone()))
4655 .collect();
4656 (inner.clone(), Some(bindings))
4657 } else {
4658 continue; // Invalid tuple
4659 }
4660 } else {
4661 (obj.clone(), None)
4662 };
4663
4664 let val = actual_obj
4665 .get(field_name)
4666 .cloned()
4667 .unwrap_or(JValue::Null);
4668
4669 if !val.is_null() {
4670 // Helper to wrap value in tuple if we have bindings
4671 let wrap_in_tuple = |v: JValue, bindings: &Option<Vec<(String, JValue)>>| -> JValue {
4672 if let Some(b) = bindings {
4673 let mut wrapper = IndexMap::new();
4674 wrapper.insert("@".to_string(), v);
4675 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
4676 for (k, val) in b {
4677 wrapper.insert(k.clone(), val.clone());
4678 }
4679 JValue::object(wrapper)
4680 } else {
4681 v
4682 }
4683 };
4684
4685 if !stages.is_empty() {
4686 // Bind this tuple's carried focus/index/ancestor
4687 // bindings so a filter predicate that references
4688 // them resolves -- e.g. `library.loans@$l.books[$l.isbn=isbn]`,
4689 // where the `[$l.isbn=isbn]` stage on the (non-focus)
4690 // `books` step must see `$l` from the enclosing
4691 // `@$l` focus stream. Without this the predicate
4692 // evaluates `$l` as unbound and filters everything out.
4693 let saved_tuple: Vec<(String, Option<JValue>)> =
4694 obj.iter()
4695 .filter_map(|(k, _)| {
4696 if let Some(n) = k.strip_prefix('$')
4697 {
4698 (!n.is_empty())
4699 .then(|| n.to_string())
4700 } else if k.starts_with('!') {
4701 Some(k.clone())
4702 } else {
4703 None
4704 }
4705 })
4706 .map(|n| {
4707 (
4708 n.clone(),
4709 self.context
4710 .lookup(&n)
4711 .cloned(),
4712 )
4713 })
4714 .collect();
4715 for (k, v) in obj.iter() {
4716 if let Some(n) = k.strip_prefix('$') {
4717 if !n.is_empty() {
4718 self.context
4719 .bind(n.to_string(), v.clone());
4720 }
4721 } else if k.starts_with('!') {
4722 self.context.bind(k.clone(), v.clone());
4723 }
4724 }
4725 // Apply stages to the extracted value
4726 let processed_val =
4727 self.apply_stages(val, stages);
4728 for (n, old) in saved_tuple.into_iter().rev() {
4729 match old {
4730 Some(v) => self.context.bind(n, v),
4731 None => self.context.unbind(&n),
4732 }
4733 }
4734 let processed_val = processed_val?;
4735 // Stages always return an array (or null); extend results
4736 match processed_val {
4737 JValue::Array(arr) => {
4738 for item in arr.iter() {
4739 result.push(wrap_in_tuple(
4740 item.clone(),
4741 &tuple_bindings,
4742 ));
4743 }
4744 }
4745 JValue::Null => {} // Skip nulls from stage application
4746 other => result.push(wrap_in_tuple(
4747 other,
4748 &tuple_bindings,
4749 )),
4750 }
4751 } else {
4752 // No stages: flatten arrays, push scalars
4753 // But preserve tuple bindings!
4754 match val {
4755 JValue::Array(arr) => {
4756 for item in arr.iter() {
4757 result.push(wrap_in_tuple(
4758 item.clone(),
4759 &tuple_bindings,
4760 ));
4761 }
4762 }
4763 other => result.push(wrap_in_tuple(
4764 other,
4765 &tuple_bindings,
4766 )),
4767 }
4768 }
4769 }
4770 }
4771 JValue::Array(_) => {
4772 // Recursively map over nested array
4773 let nested_result =
4774 self.evaluate_path(&[step.clone()], item)?;
4775 match nested_result {
4776 JValue::Array(nested) => {
4777 result.extend(nested.iter().cloned())
4778 }
4779 JValue::Null => {}
4780 other => result.push(other),
4781 }
4782 }
4783 _ => {}
4784 }
4785 }
4786
4787 JValue::array(result)
4788 }
4789 }
4790 JValue::Null => JValue::Null,
4791 // Accessing field on non-object returns undefined (not an error)
4792 _ => JValue::Undefined,
4793 }
4794 }
4795 AstNode::String(string_literal) => {
4796 // String literal as a path step - evaluate as literal and apply stages
4797 let stages = &step.stages;
4798 let val = JValue::string(string_literal.clone());
4799
4800 if !stages.is_empty() {
4801 // Apply stages (predicates) to the string literal
4802 let result = self.apply_stages(val, stages)?;
4803 // Unwrap single-element arrays back to scalar
4804 match result {
4805 JValue::Array(arr) if arr.len() == 1 => arr[0].clone(),
4806 JValue::Array(arr) if arr.is_empty() => JValue::Null,
4807 other => other,
4808 }
4809 } else {
4810 val
4811 }
4812 }
4813 AstNode::Predicate(pred_expr) => {
4814 // Predicate in path - filter or index into current value
4815 self.evaluate_predicate(¤t, pred_expr)?
4816 }
4817 AstNode::ArrayGroup(elements) => {
4818 // Array grouping: map expression over array but keep results grouped
4819 // .[expr] means evaluate expr for each array element
4820 match ¤t {
4821 JValue::Array(arr) => {
4822 let mut result = Vec::new();
4823 for item in arr.iter() {
4824 // For each array item, evaluate all elements and collect results
4825 let mut group_values = Vec::new();
4826 for element in elements {
4827 let value = self.evaluate_internal(element, item)?;
4828 // If the element is an Array/ArrayGroup, preserve its structure (don't flatten)
4829 // This ensures [[expr]] produces properly nested arrays
4830 let should_preserve_array = matches!(
4831 element,
4832 AstNode::Array(_) | AstNode::ArrayGroup(_)
4833 );
4834
4835 if should_preserve_array {
4836 // Keep the array as a single element to preserve nesting
4837 group_values.push(value);
4838 } else {
4839 // Flatten the value into group_values
4840 match value {
4841 JValue::Array(arr) => {
4842 group_values.extend(arr.iter().cloned())
4843 }
4844 other => group_values.push(other),
4845 }
4846 }
4847 }
4848 // Each array element gets its own sub-array with all values
4849 result.push(JValue::array(group_values));
4850 }
4851 // jsonata-js's evaluateStep: when this is the path's last
4852 // step and mapping produced exactly one constructed
4853 // sub-array, that sub-array IS the path result directly
4854 // (not wrapped in an outer singleton array) — e.g.
4855 // `$.[value,epochSeconds]` over a 1-element array yields
4856 // `[3, 1578381600]`, not `[[3, 1578381600]]`.
4857 if is_last_step && result.len() == 1 {
4858 result.into_iter().next().unwrap()
4859 } else {
4860 JValue::array(result)
4861 }
4862 }
4863 _ => {
4864 // For non-arrays, just evaluate the array constructor normally
4865 let mut result = Vec::new();
4866 for element in elements {
4867 let value = self.evaluate_internal(element, ¤t)?;
4868 result.push(value);
4869 }
4870 JValue::array(result)
4871 }
4872 }
4873 }
4874 AstNode::FunctionApplication(expr) => {
4875 // Function application: map expr over the current value
4876 // .(expr) means evaluate expr for each element, with $ bound to that element
4877 // Null/undefined results are filtered out
4878 //
4879 // When this parenthesized step is itself tuple-carrying (its
4880 // inner path has a `%`-tagged step, e.g. `Account.(Order.Product).{...}`),
4881 // keep the inner path's tuple wrappers so their `!label`
4882 // bindings survive to the following object/`%` step; the
4883 // end-of-path projection (or a later consumer) unwraps them.
4884 let saved_keep = self.keep_tuple_stream;
4885 if step.is_tuple {
4886 self.keep_tuple_stream = true;
4887 }
4888 let fa_result = match ¤t {
4889 JValue::Array(arr) => {
4890 // Produce the mapped result (compiled fast path or tree-walker fallback).
4891 // Do NOT return early — singleton unwrapping is applied by the outer
4892 // path evaluation code after all steps are processed.
4893 let mapped: Vec<JValue> = if let Some(compiled) = try_compile_expr(expr)
4894 {
4895 let shape = arr.first().and_then(build_shape_cache);
4896 let mut result = Vec::with_capacity(arr.len());
4897 for item in arr.iter() {
4898 let value = if let Some(ref s) = shape {
4899 eval_compiled_shaped(&compiled, item, None, s)?
4900 } else {
4901 eval_compiled(&compiled, item, None)?
4902 };
4903 if !value.is_null() && !value.is_undefined() {
4904 result.push(value);
4905 }
4906 }
4907 result
4908 } else {
4909 let mut result = Vec::new();
4910 for item in arr.iter() {
4911 // Save the current $ binding
4912 let saved_dollar = self.context.lookup("$").cloned();
4913
4914 // Bind $ to the current item
4915 self.context.bind("$".to_string(), item.clone());
4916
4917 // Evaluate the expression in the context of this item
4918 let value = self.evaluate_internal(expr, item)?;
4919
4920 // Restore the previous $ binding
4921 if let Some(saved) = saved_dollar {
4922 self.context.bind("$".to_string(), saved);
4923 } else {
4924 self.context.unbind("$");
4925 }
4926
4927 // Only include non-null/undefined values
4928 if !value.is_null() && !value.is_undefined() {
4929 result.push(value);
4930 }
4931 }
4932 result
4933 };
4934 // Don't do singleton unwrapping here - let the path result
4935 // handling deal with it, which respects has_explicit_array_keep
4936 JValue::array(mapped)
4937 }
4938 _ => {
4939 // For non-arrays, bind $ and evaluate
4940 let saved_dollar = self.context.lookup("$").cloned();
4941 self.context.bind("$".to_string(), current.clone());
4942
4943 let value = self.evaluate_internal(expr, ¤t)?;
4944
4945 if let Some(saved) = saved_dollar {
4946 self.context.bind("$".to_string(), saved);
4947 } else {
4948 self.context.unbind("$");
4949 }
4950
4951 value
4952 }
4953 };
4954 self.keep_tuple_stream = saved_keep;
4955 fa_result
4956 }
4957 AstNode::Sort { terms, .. } => {
4958 // Sort as a path step - sort 'current' by the terms
4959 self.evaluate_sort(¤t, terms)?
4960 }
4961 // Handle complex path steps (e.g., computed properties, object construction)
4962 _ => {
4963 let saved_keep = self.keep_tuple_stream;
4964 if step.is_tuple {
4965 self.keep_tuple_stream = true;
4966 }
4967 let v = self.evaluate_path_step(&step.node, ¤t, data);
4968 self.keep_tuple_stream = saved_keep;
4969 v?
4970 }
4971 };
4972 }
4973
4974 // End-of-path tuple projection, mirroring jsonata-js evaluatePath
4975 // (jsonata.js ~L202-212): once the path is a tuple stream, its VISIBLE
4976 // result is each tuple's `@` value; the `{@, $var, !label, __tuple__}`
4977 // wrappers are internal bookkeeping and must not escape into an enclosing
4978 // operator (e.g. `$#$pos[$pos<3] = $[[0..2]]`, where leaked wrappers make
4979 // `=` compare wrapper objects and always yield false). Suppressed only for
4980 // the two consumers that read the carried bindings directly off the
4981 // wrappers (Sort input, ObjectTransform/group-by input), which set
4982 // `keep_tuple_stream`. The top-level `evaluate()` still runs
4983 // `unwrap_tuple_output` as a backstop for wrappers nested inside
4984 // constructed output.
4985 if !self.keep_tuple_stream {
4986 if let JValue::Array(arr) = ¤t {
4987 let is_tuple_stream = arr.first().is_some_and(|f| {
4988 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
4989 });
4990 if is_tuple_stream {
4991 let projected: Vec<JValue> = arr
4992 .iter()
4993 .map(|t| match t {
4994 JValue::Object(o) => o.get("@").cloned().unwrap_or(JValue::Undefined),
4995 other => other.clone(),
4996 })
4997 .collect();
4998 current = JValue::array(projected);
4999 }
5000 }
5001 }
5002
5003 // JSONata singleton unwrapping: singleton results are unwrapped when we did array operations
5004 // BUT NOT when there's an explicit array-keeping operation like [] (empty predicate)
5005
5006 // Check for explicit array-keeping operations. Empty predicate `[]` can
5007 // be a `Predicate(Boolean(true))` step node or a `Filter(Boolean(true))`
5008 // stage; it also counts when it sits inside a `Sort` step's input path
5009 // (e.g. `$#$pos[][$pos<3]^($)[-1]`), whose keep-array-ness must survive
5010 // the sort and the trailing index so the singleton stays `[4]`.
5011 let has_explicit_array_keep = Self::path_keeps_singleton_array(steps);
5012
5013 // Unwrap when:
5014 // 1. Any step has stages (predicates, sorts, etc.) which are array operations, OR
5015 // 2. We did array mapping during step evaluation (tracked via did_array_mapping flag)
5016 // Note: did_array_mapping is reset to false when extracting from a single object,
5017 // so a[0].b where a[0] returns a single object and .b extracts a field will NOT unwrap.
5018 // BUT NOT when there's an explicit array-keeping operation
5019 //
5020 // Important: We DON'T unwrap just because original data was an array - what matters is
5021 // whether the final extraction was from an array mapping context or a single object.
5022 let should_unwrap = !has_explicit_array_keep
5023 && (steps.iter().any(|step| !step.stages.is_empty()) || did_array_mapping);
5024
5025 let result = match ¤t {
5026 // An empty result sequence is "no value" -> undefined (jsonata-js
5027 // treats an empty sequence, e.g. from a filter that matched nothing,
5028 // as undefined so a following `.field` and object/array construction
5029 // drop it rather than keeping an explicit null). `[]` array-keep is
5030 // handled separately above via has_explicit_array_keep.
5031 JValue::Array(arr) if arr.is_empty() => JValue::Undefined,
5032 // Unwrap singleton arrays when appropriate
5033 JValue::Array(arr) if arr.len() == 1 && should_unwrap => arr[0].clone(),
5034 // Keep arrays otherwise
5035 _ => current,
5036 };
5037
5038 // An explicit `[]` keep-array forces the result to remain an array even
5039 // after a later singleton index collapses it to a scalar (jsonata's
5040 // keepSingleton), e.g. `$#$pos[][$pos<3]^($)[-1]` must yield `[4]`.
5041 let result = if has_explicit_array_keep
5042 && !matches!(result, JValue::Array(_) | JValue::Null | JValue::Undefined)
5043 {
5044 JValue::array(vec![result])
5045 } else {
5046 result
5047 };
5048
5049 Ok(result)
5050 }
5051
5052 /// True when a path step carries a tuple-binding flag (`@$var` focus,
5053 /// `#$var` index, or a resolved `%` ancestor label) and must therefore
5054 /// produce/extend a tuple stream rather than be evaluated as a plain step.
5055 ///
5056 fn step_creates_tuple(step: &PathStep) -> bool {
5057 step.focus.is_some() || step.index_var.is_some() || step.ancestor_label.is_some()
5058 }
5059
5060 /// True when a path contains an explicit empty predicate `[]` (keep-array),
5061 /// either directly as a step/stage or nested inside a `Sort` step's input
5062 /// path. The keep-array-ness of an inner `[]` must survive an enclosing sort
5063 /// and trailing index so a singleton result stays wrapped (`$#$pos[]...^()[-1]`
5064 /// -> `[4]`).
5065 fn path_keeps_singleton_array(steps: &[PathStep]) -> bool {
5066 steps.iter().any(|step| {
5067 if let AstNode::Predicate(pred) = &step.node {
5068 if matches!(**pred, AstNode::Boolean(true)) {
5069 return true;
5070 }
5071 }
5072 if step.stages.iter().any(
5073 |s| matches!(s, Stage::Filter(pred) if matches!(**pred, AstNode::Boolean(true))),
5074 ) {
5075 return true;
5076 }
5077 if let AstNode::Sort { input, .. } = &step.node {
5078 if let AstNode::Path { steps: inner } = input.as_ref() {
5079 return Self::path_keeps_singleton_array(inner);
5080 }
5081 }
5082 false
5083 })
5084 }
5085
5086 /// Bind a tuple wrapper's carried `$name`/`!label` keys into the current
5087 /// scope, saving whatever was previously bound under each of those names
5088 /// so [`TupleKeyBindings::restore`] can put it back afterward.
5089 ///
5090 /// This is the single shared implementation of the
5091 /// "iterate a tuple wrapper's carried keys, bind, evaluate, then undo"
5092 /// pattern that recurs across `create_tuple_stream`,
5093 /// `needs_tuple_context_binding`'s handling in `evaluate_path`,
5094 /// `apply_tuple_stages`, and `evaluate_sort` -- it exists specifically so
5095 /// none of those call sites can regress to a blind `unbind` (which
5096 /// deletes rather than restores a same-named outer `:=` binding that was
5097 /// live in the same scope frame; see issue: chained `@`/`#`/sort-term
5098 /// binding silently clobbering an outer variable of the same name).
5099 fn bind_tuple_keys(&mut self, tuple_obj: &IndexMap<String, JValue>) -> TupleKeyBindings {
5100 let mut saved = Vec::new();
5101 for (key, value) in tuple_obj.iter() {
5102 let name = if let Some(n) = key.strip_prefix('$') {
5103 if n.is_empty() {
5104 continue;
5105 }
5106 n.to_string()
5107 } else if key.starts_with('!') {
5108 key.clone()
5109 } else {
5110 continue;
5111 };
5112 saved.push((name.clone(), self.context.lookup(&name).cloned()));
5113 self.context.bind(name, value.clone());
5114 }
5115 TupleKeyBindings { saved }
5116 }
5117
5118 /// Create or extend a tuple stream for a tuple-binding path step, mirroring
5119 /// jsonata-js's `evaluateTupleStep` (jsonata.js ~L315-380). The returned
5120 /// vector holds `JValue::Object` tuple wrappers of the shape
5121 /// `{ "@": value, "$focus"/"$index": ..., "!label": ..., "__tuple__": true }`
5122 /// which downstream steps consume via the existing tuple-aware handling in
5123 /// `evaluate_path`.
5124 ///
5125 /// `input` is the previous step's result: either an already-built tuple
5126 /// stream (each wrapper carried forward, per JS's `tupleBindings`) or a
5127 /// plain value/array entering tuple mode for the first time (each item
5128 /// wrapped as `{'@': item}`, per JS's `input.map(item => {'@': item})`).
5129 ///
5130 /// This is the sole *origin* of fresh `__tuple__` wrapper objects: the other
5131 /// `"__tuple__".to_string()` insert sites in `evaluate_path`'s single-field
5132 /// fast paths only *rebuild* a wrapper around a value pulled from an input
5133 /// element that is already `__tuple__`-tagged, which can only be true if a
5134 /// `create_tuple_stream` call already ran earlier in this evaluation and set
5135 /// `tuple_stream_created`. If a future edit adds a wrapping site that can
5136 /// fire on a value that did NOT come from an existing tuple stream, it must
5137 /// also set `self.tuple_stream_created = true`, or `Evaluator::evaluate`'s
5138 /// output-unwrap pass will be skipped and the wrapper will leak to callers.
5139 fn create_tuple_stream(
5140 &mut self,
5141 step: &PathStep,
5142 input: &JValue,
5143 is_first_path_step: bool,
5144 ) -> Result<Vec<JValue>, EvaluatorError> {
5145 use std::rc::Rc;
5146
5147 // Mark that this evaluate() call produced tuple wrappers, so the
5148 // top-level `evaluate()` knows to run the output-unwrap pass.
5149 self.tuple_stream_created = true;
5150
5151 // Gather the incoming tuple bindings.
5152 let is_tuple_input = matches!(
5153 input,
5154 JValue::Array(arr) if arr.first().is_some_and(|f| {
5155 matches!(f, JValue::Object(o) if o.get("__tuple__") == Some(&JValue::Bool(true)))
5156 })
5157 );
5158 let incoming: Vec<Rc<IndexMap<String, JValue>>> = if is_tuple_input {
5159 match input {
5160 JValue::Array(arr) => arr
5161 .iter()
5162 .filter_map(|t| match t {
5163 JValue::Object(o) => Some(o.clone()),
5164 _ => None,
5165 })
5166 .collect(),
5167 _ => unreachable!(),
5168 }
5169 } else {
5170 let items: Vec<JValue> = match input {
5171 // Mirrors jsonata-js evaluatePath's inputSequence rule
5172 // (`if (Array.isArray(input) && expr.steps[0].type !== 'variable')`):
5173 // when the path's FIRST step is a variable reference (`$`/`$$`) the
5174 // input array is taken as a SINGLE sequence value
5175 // (`createSequence(input)`) rather than iterated per-element. We
5176 // only need this for a leading INDEX bind (`$#$pos`): the whole
5177 // array becomes one incoming tuple whose `@` is the array, then
5178 // the inner position counter walks its elements so `$pos` runs
5179 // 0..n-1 (not 0 for every singleton). A leading FOCUS bind
5180 // (`$@$i`) must instead iterate per-element -- focus keeps `@` as
5181 // the step input, so a single binding would yield one copy of the
5182 // whole array per element (`$@$i` on [1,2,3] must give [1,2,3],
5183 // not [[1,2,3],[1,2,3],[1,2,3]]). The rule is scoped to step 0 so
5184 // `$.$#$pos` (a later step) still iterates per-element.
5185 JValue::Array(arr)
5186 if !(is_first_path_step
5187 && matches!(&step.node, AstNode::Variable(_))
5188 && step.index_var.is_some()) =>
5189 {
5190 arr.iter().cloned().collect()
5191 }
5192 single => vec![single.clone()],
5193 };
5194 items
5195 .into_iter()
5196 .map(|item| {
5197 let mut wrapper = IndexMap::new();
5198 wrapper.insert("@".to_string(), item);
5199 wrapper.insert("__tuple__".to_string(), JValue::Bool(true));
5200 Rc::new(wrapper)
5201 })
5202 .collect()
5203 };
5204
5205 // A sort step in a tuple stream orders the WHOLE stream (not per element)
5206 // and re-tuples with the index = sorted position, mirroring jsonata-js
5207 // evaluateTupleStep's `sort` case. `$^($)#$pos[$pos<3]` must sort the
5208 // array, then number the sorted values, then filter by `$pos`.
5209 if let AstNode::Sort { terms, .. } = &step.node {
5210 let stream = JValue::array(
5211 incoming
5212 .iter()
5213 .map(|t| JValue::object((**t).clone()))
5214 .collect(),
5215 );
5216 // evaluate_sort is tuple-aware (orders by each wrapper's `@`, with the
5217 // carried keys bound), returning the wrappers in sorted order.
5218 let sorted = self.evaluate_sort(&stream, terms)?;
5219 let sorted_arr: Vec<JValue> = match sorted {
5220 JValue::Array(a) => a.iter().cloned().collect(),
5221 JValue::Null | JValue::Undefined => Vec::new(),
5222 other => vec![other],
5223 };
5224 let mut result = Vec::new();
5225 for (ss, elem) in sorted_arr.into_iter().enumerate() {
5226 let mut new_tuple = match elem {
5227 JValue::Object(o) => (*o).clone(),
5228 other => {
5229 let mut m = IndexMap::new();
5230 m.insert("@".to_string(), other);
5231 m
5232 }
5233 };
5234 if let Some(index_var) = &step.index_var {
5235 new_tuple.insert(format!("${}", index_var), JValue::from(ss as i64));
5236 }
5237 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5238 result.push(JValue::object(new_tuple));
5239 }
5240 return Ok(result);
5241 }
5242
5243 let mut result = Vec::new();
5244 for tuple_obj in incoming {
5245 // Bind every carried tuple key into a real scope frame so the step
5246 // expression can see prior focus/index/ancestor bindings, mirroring
5247 // createFrameFromTuple's "for every key in tuple, frame.bind(...)".
5248 // Saves/restores rather than blindly unbinding, so a tuple key
5249 // whose name collides with a live outer `:=` binding doesn't get
5250 // deleted once this tuple row's evaluation is done.
5251 let tuple_bindings = self.bind_tuple_keys(&tuple_obj);
5252
5253 let actual_data = tuple_obj.get("@").cloned().unwrap_or(JValue::Undefined);
5254 let step_value = self.evaluate_internal(&step.node, &actual_data);
5255
5256 let mut step_value = step_value?;
5257 // When the step carries an ORDERED index stage (a second `#$var`,
5258 // e.g. `books@$b#$ib[...]#$ib2`), its stages must be applied to the
5259 // BUILT tuple stream in order (filter then re-number) so the filter
5260 // sees the per-tuple focus/index bindings and each index reflects the
5261 // position at its point in the sequence. Those steps defer all stage
5262 // application to `apply_tuple_stages` after the stream is built.
5263 let has_index_stage = step.stages.iter().any(|s| matches!(s, Stage::Index(_)));
5264 if !step.stages.is_empty() && !has_index_stage {
5265 // A `%` inside a filter predicate refers to the ancestry of
5266 // THIS step (its own input for a level-1 `%`, or an earlier
5267 // step's input for a `%.%` chain). ast_transform tags this step
5268 // with `ancestor_label`; bind it to the step's input so the
5269 // level-1 `%` resolves. The `%.%` chain's deeper references use
5270 // labels carried in the INCOMING tuple, so those bindings
5271 // (`tuple_bindings`) must stay live through `apply_stages` --
5272 // their restore is deferred until after it (previously they
5273 // were unbound first, which silently broke `%.%` inside
5274 // predicates).
5275 let own_label = match &step.ancestor_label {
5276 Some(label) if !tuple_bindings.contains(label) => {
5277 self.context.bind(label.clone(), actual_data.clone());
5278 Some(label.clone())
5279 }
5280 _ => None,
5281 };
5282 step_value = self.apply_stages(step_value, &step.stages)?;
5283 if let Some(label) = own_label {
5284 self.context.unbind(&label);
5285 }
5286 }
5287
5288 tuple_bindings.restore(self);
5289
5290 let row: Vec<JValue> = match step_value {
5291 JValue::Undefined => continue,
5292 JValue::Array(arr) => arr.iter().cloned().collect(),
5293 other => vec![other],
5294 };
5295
5296 for (position, value) in row.into_iter().enumerate() {
5297 if value.is_undefined() {
5298 continue;
5299 }
5300 let mut new_tuple = (*tuple_obj).clone();
5301 if let Some(focus_var) = &step.focus {
5302 // Focus binding keeps `@` as this step's INPUT (already carried
5303 // in the cloned tuple) and binds the result to `$focus`,
5304 // matching jsonata-js: `tuple[expr.focus] = res[bb];
5305 // tuple['@'] = tupleBindings[ee]['@'];`.
5306 new_tuple.insert(format!("${}", focus_var), value);
5307 } else {
5308 new_tuple.insert("@".to_string(), value);
5309 }
5310 if let Some(index_var) = &step.index_var {
5311 // Index binding records the position of this value WITHIN the
5312 // per-binding result row (jsonata-js evaluateTupleStep: the
5313 // inner `bb` counter, `tuple[expr.index] = bb`), which resets
5314 // for each incoming tuple.
5315 new_tuple.insert(format!("${}", index_var), JValue::from(position as i64));
5316 }
5317 if let Some(ancestor_label) = &step.ancestor_label {
5318 // `%` ancestor: preserve this step's INPUT under the label.
5319 new_tuple.insert(ancestor_label.clone(), actual_data.clone());
5320 }
5321 new_tuple.insert("__tuple__".to_string(), JValue::Bool(true));
5322 result.push(JValue::object(new_tuple));
5323 }
5324 }
5325
5326 // Apply ordered filter/index stages to the built tuple stream when a
5327 // second index binding deferred them (see the has_index_stage comment
5328 // in the build loop above).
5329 if step.stages.iter().any(|s| matches!(s, Stage::Index(_))) {
5330 result = self.apply_tuple_stages(result, &step.stages)?;
5331 }
5332
5333 Ok(result)
5334 }
5335
5336 /// Apply a step's stages, in order, to an already-built tuple stream --
5337 /// mirrors jsonata-js `evaluateStages` (jsonata.js ~L288-305): a `filter`
5338 /// keeps the tuples whose predicate is truthy (evaluated against each tuple's
5339 /// `@` with its carried `$var`/`!label` bindings in scope), and an `index`
5340 /// stage sets its variable on every surviving tuple to that tuple's position
5341 /// in the CURRENT stream. Used for steps carrying a second `#$var` index
5342 /// binding (e.g. `books@$b#$ib[$l.isbn=$b.isbn]#$ib2`), where `$ib` is the
5343 /// pre-filter position and `$ib2` the post-filter position.
5344 fn apply_tuple_stages(
5345 &mut self,
5346 mut tuples: Vec<JValue>,
5347 stages: &[Stage],
5348 ) -> Result<Vec<JValue>, EvaluatorError> {
5349 for stage in stages {
5350 match stage {
5351 Stage::Filter(pred) => {
5352 let mut kept = Vec::with_capacity(tuples.len());
5353 for tup in tuples.into_iter() {
5354 let JValue::Object(obj) = &tup else {
5355 continue;
5356 };
5357 // Bind this tuple's carried focus/index/ancestor keys so
5358 // the predicate can reference them (save/restore rather
5359 // than blind unbind -- see bind_tuple_keys).
5360 let tuple_bindings = self.bind_tuple_keys(obj);
5361 let at = obj.get("@").cloned().unwrap_or(JValue::Undefined);
5362 let pred_res = self.evaluate_internal(pred, &at);
5363 tuple_bindings.restore(self);
5364 if self.is_truthy(&pred_res?) {
5365 kept.push(tup);
5366 }
5367 }
5368 tuples = kept;
5369 }
5370 Stage::Index(var) => {
5371 for (pos, tup) in tuples.iter_mut().enumerate() {
5372 if let JValue::Object(obj) = tup {
5373 let mut m = (**obj).clone();
5374 m.insert(format!("${}", var), JValue::from(pos as i64));
5375 *tup = JValue::object(m);
5376 }
5377 }
5378 }
5379 }
5380 }
5381 Ok(tuples)
5382 }
5383
5384 /// Helper to evaluate a complex path step
5385 fn evaluate_path_step(
5386 &mut self,
5387 step: &AstNode,
5388 current: &JValue,
5389 original_data: &JValue,
5390 ) -> Result<JValue, EvaluatorError> {
5391 // Special case: array mapping with object construction
5392 // e.g., items.{"name": name, "price": price}
5393 if matches!(current, JValue::Array(_)) && matches!(step, AstNode::Object(_)) {
5394 match (current, step) {
5395 (JValue::Array(arr), AstNode::Object(pairs)) => {
5396 // Try CompiledExpr for object construction (handles arithmetic, conditionals, etc.)
5397 if let Some(compiled) = try_compile_expr(&AstNode::Object(pairs.clone())) {
5398 let shape = arr.first().and_then(build_shape_cache);
5399 let mut mapped = Vec::with_capacity(arr.len());
5400 for item in arr.iter() {
5401 let result = if let Some(ref s) = shape {
5402 eval_compiled_shaped(&compiled, item, None, s)?
5403 } else {
5404 eval_compiled(&compiled, item, None)?
5405 };
5406 if !result.is_undefined() {
5407 mapped.push(result);
5408 }
5409 }
5410 return Ok(JValue::array(mapped));
5411 }
5412 // Fallback: full AST evaluation per element
5413 let mapped: Result<Vec<JValue>, EvaluatorError> = arr
5414 .iter()
5415 .map(|item| self.evaluate_internal(step, item))
5416 .collect();
5417 Ok(JValue::array(mapped?))
5418 }
5419 _ => unreachable!(),
5420 }
5421 } else {
5422 // Special case: array.$ should map $ over the array, returning each element
5423 // e.g., [1, 2, 3].$ returns [1, 2, 3]
5424 if let AstNode::Variable(name) = step {
5425 if name.is_empty() {
5426 // Bare $ - map over array if current is an array
5427 if let JValue::Array(arr) = current {
5428 // Map $ over each element - $ refers to each element in turn
5429 return Ok(JValue::Array(arr.clone()));
5430 } else {
5431 // For non-arrays, $ refers to the current value
5432 return Ok(current.clone());
5433 }
5434 }
5435 }
5436
5437 // Special case: Variable access on tuple arrays (from index binding #$var)
5438 // When current is a tuple array, we need to evaluate the variable against each tuple
5439 // so that tuple bindings ($i, etc.) can be found
5440 if matches!(step, AstNode::Variable(_)) {
5441 if let JValue::Array(arr) = current {
5442 // Check if this is a tuple array
5443 let is_tuple_array = arr.first().is_some_and(|first| {
5444 if let JValue::Object(obj) = first {
5445 obj.get("__tuple__") == Some(&JValue::Bool(true))
5446 } else {
5447 false
5448 }
5449 });
5450
5451 if is_tuple_array {
5452 // Map the variable lookup over each tuple
5453 let mut results = Vec::new();
5454 for tuple in arr.iter() {
5455 // Evaluate the variable in the context of this tuple
5456 // This allows tuple bindings ($i, etc.) to be found
5457 let val = self.evaluate_internal(step, tuple)?;
5458 if !val.is_null() && !val.is_undefined() {
5459 results.push(val);
5460 }
5461 }
5462 return Ok(JValue::array(results));
5463 }
5464 }
5465 }
5466
5467 // For certain operations (Binary, Function calls, Variables, ParentVariables, Arrays, Objects, Sort, Blocks), the step evaluates to a new value
5468 // rather than being used to index/access the current value
5469 // e.g., items[price > 50] where [price > 50] is a filter operation
5470 // or $x.price where $x is a variable binding
5471 // or $$.field where $$ is the parent context
5472 // or [0..9] where it's an array constructor
5473 // or $^(field) where it's a sort operator
5474 // or (expr).field where (expr) is a block that evaluates to a value
5475 if matches!(
5476 step,
5477 AstNode::Binary { .. }
5478 | AstNode::Function { .. }
5479 | AstNode::Variable(_)
5480 | AstNode::ParentVariable(_)
5481 | AstNode::Parent(_)
5482 | AstNode::Array(_)
5483 | AstNode::Object(_)
5484 | AstNode::Sort { .. }
5485 | AstNode::Block(_)
5486 ) {
5487 // Evaluate the step in the context of original_data and return the result directly
5488 return self.evaluate_internal(step, original_data);
5489 }
5490
5491 // Standard path step evaluation for indexing/accessing current value
5492 let step_value = self.evaluate_internal(step, original_data)?;
5493 Ok(match (current, &step_value) {
5494 (JValue::Object(obj), JValue::String(key)) => {
5495 obj.get(&**key).cloned().unwrap_or(JValue::Undefined)
5496 }
5497 (JValue::Array(arr), JValue::Number(n)) => {
5498 let index = *n as i64;
5499 let len = arr.len() as i64;
5500
5501 // Handle negative indexing (offset from end)
5502 let actual_idx = if index < 0 { len + index } else { index };
5503
5504 if actual_idx < 0 || actual_idx >= len {
5505 JValue::Undefined
5506 } else {
5507 arr[actual_idx as usize].clone()
5508 }
5509 }
5510 _ => JValue::Undefined,
5511 })
5512 }
5513 }
5514
5515 /// Evaluate a binary operation
5516 fn evaluate_binary_op(
5517 &mut self,
5518 op: crate::ast::BinaryOp,
5519 lhs: &AstNode,
5520 rhs: &AstNode,
5521 data: &JValue,
5522 ) -> Result<JValue, EvaluatorError> {
5523 use crate::ast::BinaryOp;
5524
5525 // Special handling for coalescing operator (??)
5526 // Returns right side if left is undefined (produces no value)
5527 // Note: literal null is a value, so it's NOT replaced
5528 if op == BinaryOp::Coalesce {
5529 // Try to evaluate the left side
5530 return match self.evaluate_internal(lhs, data) {
5531 Ok(value) => {
5532 // Successfully evaluated to a value (even if it's null)
5533 // Check if LHS is a literal null - keep it (null is a value, not undefined)
5534 if matches!(lhs, AstNode::Null) {
5535 Ok(value)
5536 }
5537 // For paths and variables, undefined (no match/unbound) - use RHS
5538 else if value.is_undefined()
5539 && (matches!(lhs, AstNode::Path { .. })
5540 || matches!(lhs, AstNode::String(_))
5541 || matches!(lhs, AstNode::Variable(_)))
5542 {
5543 self.evaluate_internal(rhs, data)
5544 } else {
5545 Ok(value)
5546 }
5547 }
5548 Err(_) => {
5549 // Evaluation failed (e.g., undefined variable) - use RHS
5550 self.evaluate_internal(rhs, data)
5551 }
5552 };
5553 }
5554
5555 // Special handling for default operator (?:)
5556 // Returns right side if left is falsy or a non-value (like a function)
5557 if op == BinaryOp::Default {
5558 let left = self.evaluate_internal(lhs, data)?;
5559 if self.is_truthy_for_default(&left) {
5560 return Ok(left);
5561 }
5562 return self.evaluate_internal(rhs, data);
5563 }
5564
5565 // Special handling for chain/pipe operator (~>)
5566 // Pipes the LHS result to the RHS function as the first argument
5567 // e.g., expr ~> func(arg2) becomes func(expr, arg2)
5568 if op == BinaryOp::ChainPipe {
5569 // Handle regex on RHS - treat as $match(lhs, regex)
5570 if let AstNode::Regex { pattern, flags } = rhs {
5571 // Evaluate LHS
5572 let lhs_value = self.evaluate_internal(lhs, data)?;
5573 // Do regex match inline
5574 return match lhs_value {
5575 JValue::String(s) => {
5576 // Build the regex
5577 let case_insensitive = flags.contains('i');
5578 let regex_pattern = if case_insensitive {
5579 format!("(?i){}", pattern)
5580 } else {
5581 pattern.clone()
5582 };
5583 match regex::Regex::new(®ex_pattern) {
5584 Ok(re) => {
5585 if let Some(m) = re.find(&s) {
5586 // Return match object
5587 let mut result = IndexMap::new();
5588 result.insert(
5589 "match".to_string(),
5590 JValue::string(m.as_str().to_string()),
5591 );
5592 result.insert(
5593 "start".to_string(),
5594 JValue::Number(m.start() as f64),
5595 );
5596 result
5597 .insert("end".to_string(), JValue::Number(m.end() as f64));
5598
5599 // Capture groups
5600 let mut groups = Vec::new();
5601 for cap in re.captures_iter(&s).take(1) {
5602 for i in 1..cap.len() {
5603 if let Some(c) = cap.get(i) {
5604 groups.push(JValue::string(c.as_str().to_string()));
5605 }
5606 }
5607 }
5608 if !groups.is_empty() {
5609 result.insert("groups".to_string(), JValue::array(groups));
5610 }
5611
5612 Ok(JValue::object(result))
5613 } else {
5614 Ok(JValue::Null)
5615 }
5616 }
5617 Err(e) => Err(EvaluatorError::EvaluationError(format!(
5618 "Invalid regex: {}",
5619 e
5620 ))),
5621 }
5622 }
5623 JValue::Null => Ok(JValue::Null),
5624 _ => Err(EvaluatorError::TypeError(
5625 "Left side of ~> /regex/ must be a string".to_string(),
5626 )),
5627 };
5628 }
5629
5630 // Early check: if LHS evaluates to undefined, return undefined
5631 // This matches JSONata behavior where undefined ~> anyFunc returns undefined
5632 let lhs_value_for_check = self.evaluate_internal(lhs, data)?;
5633 if lhs_value_for_check.is_undefined() || lhs_value_for_check.is_null() {
5634 return Ok(JValue::Undefined);
5635 }
5636
5637 // Handle different RHS types
5638 match rhs {
5639 AstNode::Function {
5640 name,
5641 args,
5642 is_builtin,
5643 } => {
5644 // RHS is a function call
5645 // Check if the function call has placeholder arguments (partial application)
5646 let has_placeholder =
5647 args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
5648
5649 if has_placeholder {
5650 // Partial application: replace the first placeholder with LHS value
5651 let lhs_value = self.evaluate_internal(lhs, data)?;
5652 let mut filled_args = Vec::new();
5653 let mut lhs_used = false;
5654
5655 for arg in args.iter() {
5656 if matches!(arg, AstNode::Placeholder) && !lhs_used {
5657 // Replace first placeholder with evaluated LHS
5658 // We need to create a temporary binding to pass the value
5659 let temp_name = format!("__pipe_arg_{}", filled_args.len());
5660 self.context.bind(temp_name.clone(), lhs_value.clone());
5661 filled_args.push(AstNode::Variable(temp_name));
5662 lhs_used = true;
5663 } else {
5664 filled_args.push(arg.clone());
5665 }
5666 }
5667
5668 // Evaluate the function with filled args
5669 let result =
5670 self.evaluate_function_call(name, &filled_args, *is_builtin, data);
5671
5672 // Clean up temp bindings
5673 for (i, arg) in args.iter().enumerate() {
5674 if matches!(arg, AstNode::Placeholder) {
5675 self.context.unbind(&format!("__pipe_arg_{}", i));
5676 }
5677 }
5678
5679 // Unwrap singleton results from chain operator
5680 return result.map(|v| self.unwrap_singleton(v));
5681 } else {
5682 // No placeholders: build args list with LHS as first argument
5683 let mut all_args = vec![lhs.clone()];
5684 all_args.extend_from_slice(args);
5685 // Unwrap singleton results from chain operator
5686 return self
5687 .evaluate_function_call(name, &all_args, *is_builtin, data)
5688 .map(|v| self.unwrap_singleton(v));
5689 }
5690 }
5691 AstNode::Variable(var_name) => {
5692 // RHS is a function reference (no parens)
5693 // e.g., $average($tempReadings) ~> $round
5694 let all_args = vec![lhs.clone()];
5695 // Unwrap singleton results from chain operator
5696 return self
5697 .evaluate_function_call(var_name, &all_args, true, data)
5698 .map(|v| self.unwrap_singleton(v));
5699 }
5700 AstNode::Binary {
5701 op: BinaryOp::ChainPipe,
5702 ..
5703 } => {
5704 // RHS is another chain pipe - evaluate LHS first, then pipe through RHS
5705 // e.g., x ~> (f1 ~> f2) => (x ~> f1) ~> f2
5706 let lhs_value = self.evaluate_internal(lhs, data)?;
5707 return self.evaluate_internal(rhs, &lhs_value);
5708 }
5709 AstNode::Transform { .. } => {
5710 // RHS is a transform - invoke it with LHS as input
5711 // Evaluate LHS first
5712 let lhs_value = self.evaluate_internal(lhs, data)?;
5713
5714 // Bind $ to the LHS value, then evaluate the transform
5715 let saved_binding = self.context.lookup("$").cloned();
5716 self.context.bind("$".to_string(), lhs_value.clone());
5717
5718 let result = self.evaluate_internal(rhs, data);
5719
5720 // Restore $ binding
5721 if let Some(saved) = saved_binding {
5722 self.context.bind("$".to_string(), saved);
5723 } else {
5724 self.context.unbind("$");
5725 }
5726
5727 // Unwrap singleton results from chain operator
5728 return result.map(|v| self.unwrap_singleton(v));
5729 }
5730 AstNode::Lambda {
5731 params,
5732 body,
5733 signature,
5734 thunk,
5735 } => {
5736 // RHS is a lambda - invoke it with LHS as argument
5737 let lhs_value = self.evaluate_internal(lhs, data)?;
5738 // Unwrap singleton results from chain operator
5739 return self
5740 .invoke_lambda(params, body, signature.as_ref(), &[lhs_value], data, *thunk)
5741 .map(|v| self.unwrap_singleton(v));
5742 }
5743 AstNode::Path { steps } => {
5744 // RHS is a path expression (e.g., function call with predicate: $map($f)[])
5745 // If the first step is a function call, we need to add LHS as first argument
5746 if let Some(first_step) = steps.first() {
5747 match &first_step.node {
5748 AstNode::Function {
5749 name,
5750 args,
5751 is_builtin,
5752 } => {
5753 // Prepend LHS to the function arguments
5754 let mut all_args = vec![lhs.clone()];
5755 all_args.extend_from_slice(args);
5756
5757 // Call the function
5758 let mut result = self.evaluate_function_call(
5759 name,
5760 &all_args,
5761 *is_builtin,
5762 data,
5763 )?;
5764
5765 // Apply stages from the first step (e.g., predicates)
5766 for stage in &first_step.stages {
5767 match stage {
5768 Stage::Filter(filter_expr) => {
5769 result = self.evaluate_predicate_as_stage(
5770 &result,
5771 filter_expr,
5772 )?;
5773 }
5774 Stage::Index(_) => {}
5775 }
5776 }
5777
5778 // Apply remaining path steps if any
5779 if steps.len() > 1 {
5780 let remaining_path = AstNode::Path {
5781 steps: steps[1..].to_vec(),
5782 };
5783 result = self.evaluate_internal(&remaining_path, &result)?;
5784 }
5785
5786 // Unwrap singleton results from chain operator, unless there are stages
5787 // Stages (like predicates) indicate we want to preserve array structure
5788 if !first_step.stages.is_empty() || steps.len() > 1 {
5789 return Ok(result);
5790 } else {
5791 return Ok(self.unwrap_singleton(result));
5792 }
5793 }
5794 AstNode::Variable(var_name) => {
5795 // Variable that should resolve to a function
5796 let all_args = vec![lhs.clone()];
5797 let mut result =
5798 self.evaluate_function_call(var_name, &all_args, true, data)?;
5799
5800 // Apply stages from the first step
5801 for stage in &first_step.stages {
5802 match stage {
5803 Stage::Filter(filter_expr) => {
5804 result = self.evaluate_predicate_as_stage(
5805 &result,
5806 filter_expr,
5807 )?;
5808 }
5809 Stage::Index(_) => {}
5810 }
5811 }
5812
5813 // Apply remaining path steps if any
5814 if steps.len() > 1 {
5815 let remaining_path = AstNode::Path {
5816 steps: steps[1..].to_vec(),
5817 };
5818 result = self.evaluate_internal(&remaining_path, &result)?;
5819 }
5820
5821 // Unwrap singleton results from chain operator, unless there are stages
5822 // Stages (like predicates) indicate we want to preserve array structure
5823 if !first_step.stages.is_empty() || steps.len() > 1 {
5824 return Ok(result);
5825 } else {
5826 return Ok(self.unwrap_singleton(result));
5827 }
5828 }
5829 _ => {
5830 // Other path types - just evaluate normally with LHS as context
5831 let lhs_value = self.evaluate_internal(lhs, data)?;
5832 return self
5833 .evaluate_internal(rhs, &lhs_value)
5834 .map(|v| self.unwrap_singleton(v));
5835 }
5836 }
5837 }
5838
5839 // Empty path? Shouldn't happen, but handle it
5840 let lhs_value = self.evaluate_internal(lhs, data)?;
5841 return self
5842 .evaluate_internal(rhs, &lhs_value)
5843 .map(|v| self.unwrap_singleton(v));
5844 }
5845 _ => {
5846 return Err(EvaluatorError::TypeError(
5847 "Right side of ~> must be a function call or function reference"
5848 .to_string(),
5849 ));
5850 }
5851 }
5852 }
5853
5854 // Special handling for variable binding (:=)
5855 if op == BinaryOp::ColonEqual {
5856 // Extract variable name from LHS
5857 let var_name = match lhs {
5858 AstNode::Variable(name) => name.clone(),
5859 _ => {
5860 return Err(EvaluatorError::TypeError(
5861 "Left side of := must be a variable".to_string(),
5862 ))
5863 }
5864 };
5865
5866 // Check if RHS is a lambda - store it specially
5867 if let AstNode::Lambda {
5868 params,
5869 body,
5870 signature,
5871 thunk,
5872 } = rhs
5873 {
5874 // Store the lambda AST for later invocation
5875 // Capture only the free variables referenced by the lambda body
5876 let captured_env = self.capture_environment_for(body, params);
5877 let compiled_body = if !thunk {
5878 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
5879 try_compile_expr_with_allowed_vars(body, &var_refs)
5880 } else {
5881 None
5882 };
5883 let stored_lambda = StoredLambda {
5884 params: params.clone(),
5885 body: (**body).clone(),
5886 compiled_body,
5887 signature: signature.clone(),
5888 captured_env,
5889 captured_data: Some(data.clone()),
5890 thunk: *thunk,
5891 };
5892 let lambda_params = stored_lambda.params.clone();
5893 let lambda_sig = stored_lambda.signature.clone();
5894 self.context.bind_lambda(var_name.clone(), stored_lambda);
5895
5896 // Return a lambda marker value (include _lambda_id so it can be found later)
5897 let lambda_repr = JValue::lambda(
5898 var_name.as_str(),
5899 lambda_params,
5900 Some(var_name.clone()),
5901 lambda_sig,
5902 );
5903 return Ok(lambda_repr);
5904 }
5905
5906 // Check if RHS is a pure function composition (ChainPipe between function references)
5907 // e.g., $uppertrim := $trim ~> $uppercase
5908 // This creates a lambda that composes the functions.
5909 // But NOT for data ~> function, which should be evaluated immediately.
5910 // e.g., $result := data ~> $map($fn) should evaluate the pipe
5911 if let AstNode::Binary {
5912 op: BinaryOp::ChainPipe,
5913 lhs: chain_lhs,
5914 rhs: chain_rhs,
5915 } = rhs
5916 {
5917 // Only wrap in lambda if LHS is a function reference (Variable pointing to a function)
5918 // If LHS is data (array, object, function call result, etc.), evaluate the pipe
5919 let is_function_composition = match chain_lhs.as_ref() {
5920 // LHS is a function reference like $trim or $sum
5921 AstNode::Variable(name)
5922 if self.is_builtin_function(name)
5923 || self.context.lookup_lambda(name).is_some() =>
5924 {
5925 true
5926 }
5927 // LHS is another ChainPipe (nested composition like $f ~> $g ~> $h)
5928 AstNode::Binary {
5929 op: BinaryOp::ChainPipe,
5930 ..
5931 } => true,
5932 // A function call with placeholder creates a partial application
5933 // e.g., $substringAfter(?, "@") ~> $substringBefore(?, ".")
5934 AstNode::Function { args, .. }
5935 if args.iter().any(|a| matches!(a, AstNode::Placeholder)) =>
5936 {
5937 true
5938 }
5939 // Anything else (data, function calls, arrays, etc.) is not pure composition
5940 _ => false,
5941 };
5942
5943 if is_function_composition {
5944 // Create a lambda: function($) { ($ ~> firstFunc) ~> restOfChain }
5945 // The original chain is $trim ~> $uppercase (left-associative)
5946 // We want to create: ($ ~> $trim) ~> $uppercase
5947 let param_name = "$".to_string();
5948
5949 // First create $ ~> $trim
5950 let first_pipe = AstNode::Binary {
5951 op: BinaryOp::ChainPipe,
5952 lhs: Box::new(AstNode::Variable(param_name.clone())),
5953 rhs: chain_lhs.clone(),
5954 };
5955
5956 // Then wrap with ~> $uppercase (or the rest of the chain)
5957 let composed_body = AstNode::Binary {
5958 op: BinaryOp::ChainPipe,
5959 lhs: Box::new(first_pipe),
5960 rhs: chain_rhs.clone(),
5961 };
5962
5963 let stored_lambda = StoredLambda {
5964 params: vec![param_name],
5965 body: composed_body,
5966 compiled_body: None, // ChainPipe body is not compilable
5967 signature: None,
5968 captured_env: self.capture_current_environment(),
5969 captured_data: Some(data.clone()),
5970 thunk: false,
5971 };
5972 self.context.bind_lambda(var_name.clone(), stored_lambda);
5973
5974 // Return a lambda marker value (include _lambda_id for later lookup)
5975 let lambda_repr = JValue::lambda(
5976 var_name.as_str(),
5977 vec!["$".to_string()],
5978 Some(var_name.clone()),
5979 None::<String>,
5980 );
5981 return Ok(lambda_repr);
5982 }
5983 // If not function composition, fall through to normal evaluation below
5984 }
5985
5986 // Evaluate the RHS
5987 let value = self.evaluate_internal(rhs, data)?;
5988
5989 // If the value is a lambda, copy the stored lambda to the new variable name
5990 if let Some(stored) = self.lookup_lambda_from_value(&value) {
5991 self.context.bind_lambda(var_name.clone(), stored);
5992 }
5993
5994 // Bind even if undefined (null) so inner scopes can shadow outer variables
5995 self.context.bind(var_name, value.clone());
5996 return Ok(value);
5997 }
5998
5999 // Special handling for 'In' operator - check for array filtering
6000 // Must evaluate lhs first to determine if this is array filtering
6001 if op == BinaryOp::In {
6002 let left = self.evaluate_internal(lhs, data)?;
6003
6004 // Check if this is array filtering: array[predicate]
6005 if matches!(left, JValue::Array(_)) {
6006 // Try evaluating rhs in current context to see if it's a simple index
6007 let right_result = self.evaluate_internal(rhs, data);
6008
6009 if let Ok(JValue::Number(_)) = right_result {
6010 // Simple numeric index: array[n]
6011 return self.array_index(&left, &right_result.unwrap());
6012 } else {
6013 // This is array filtering: array[predicate]
6014 // Evaluate the predicate for each array item
6015 return self.array_filter(lhs, rhs, &left, data);
6016 }
6017 }
6018 }
6019
6020 // Special handling for logical operators (short-circuit evaluation)
6021 if op == BinaryOp::And {
6022 let left = self.evaluate_internal(lhs, data)?;
6023 if !self.is_truthy(&left) {
6024 // Short-circuit: if left is falsy, return false without evaluating right
6025 return Ok(JValue::Bool(false));
6026 }
6027 let right = self.evaluate_internal(rhs, data)?;
6028 return Ok(JValue::Bool(self.is_truthy(&right)));
6029 }
6030
6031 if op == BinaryOp::Or {
6032 let left = self.evaluate_internal(lhs, data)?;
6033 if self.is_truthy(&left) {
6034 // Short-circuit: if left is truthy, return true without evaluating right
6035 return Ok(JValue::Bool(true));
6036 }
6037 let right = self.evaluate_internal(rhs, data)?;
6038 return Ok(JValue::Bool(self.is_truthy(&right)));
6039 }
6040
6041 // Check if operands are explicit null literals (vs undefined from variables)
6042 let left_is_explicit_null = matches!(lhs, AstNode::Null);
6043 let right_is_explicit_null = matches!(rhs, AstNode::Null);
6044
6045 // Standard evaluation: evaluate both operands
6046 let left = self.evaluate_internal(lhs, data)?;
6047 let right = self.evaluate_internal(rhs, data)?;
6048
6049 match op {
6050 BinaryOp::Add => self.add(&left, &right, left_is_explicit_null, right_is_explicit_null),
6051 BinaryOp::Subtract => {
6052 self.subtract(&left, &right, left_is_explicit_null, right_is_explicit_null)
6053 }
6054 BinaryOp::Multiply => {
6055 self.multiply(&left, &right, left_is_explicit_null, right_is_explicit_null)
6056 }
6057 BinaryOp::Divide => {
6058 self.divide(&left, &right, left_is_explicit_null, right_is_explicit_null)
6059 }
6060 BinaryOp::Modulo => {
6061 self.modulo(&left, &right, left_is_explicit_null, right_is_explicit_null)
6062 }
6063
6064 BinaryOp::Equal => Ok(JValue::Bool(self.equals(&left, &right))),
6065 BinaryOp::NotEqual => Ok(JValue::Bool(!self.equals(&left, &right))),
6066 BinaryOp::LessThan => {
6067 self.less_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6068 }
6069 BinaryOp::LessThanOrEqual => self.less_than_or_equal(
6070 &left,
6071 &right,
6072 left_is_explicit_null,
6073 right_is_explicit_null,
6074 ),
6075 BinaryOp::GreaterThan => {
6076 self.greater_than(&left, &right, left_is_explicit_null, right_is_explicit_null)
6077 }
6078 BinaryOp::GreaterThanOrEqual => self.greater_than_or_equal(
6079 &left,
6080 &right,
6081 left_is_explicit_null,
6082 right_is_explicit_null,
6083 ),
6084
6085 // And/Or handled above with short-circuit evaluation
6086 BinaryOp::And | BinaryOp::Or => unreachable!(),
6087
6088 BinaryOp::Concatenate => self.concatenate(&left, &right),
6089 BinaryOp::Range => self.range(&left, &right),
6090 BinaryOp::In => self.in_operator(&left, &right),
6091
6092 // Focus binding: should be resolved by ast_transform pass (Task 2)
6093 BinaryOp::FocusBind => Err(EvaluatorError::EvaluationError(
6094 "Focus binding operator (@) must be resolved by ast_transform pass".to_string(),
6095 )),
6096
6097 // Index binding: should be resolved by ast_transform pass (Task 4,
6098 // which retired the dedicated AstNode::IndexBind variant in favor
6099 // of this generic Binary marker, mirroring FocusBind above)
6100 BinaryOp::IndexBind => Err(EvaluatorError::EvaluationError(
6101 "Index binding operator (#) must be resolved by ast_transform pass".to_string(),
6102 )),
6103
6104 // These operators are all handled as special cases earlier in evaluate_binary_op
6105 BinaryOp::ColonEqual | BinaryOp::Coalesce | BinaryOp::Default | BinaryOp::ChainPipe => {
6106 unreachable!()
6107 }
6108 }
6109 }
6110
6111 /// Evaluate a unary operation
6112 fn evaluate_unary_op(
6113 &mut self,
6114 op: crate::ast::UnaryOp,
6115 operand: &AstNode,
6116 data: &JValue,
6117 ) -> Result<JValue, EvaluatorError> {
6118 use crate::ast::UnaryOp;
6119
6120 let value = self.evaluate_internal(operand, data)?;
6121
6122 match op {
6123 UnaryOp::Negate => match value {
6124 // undefined returns undefined
6125 JValue::Null | JValue::Undefined => Ok(JValue::Null),
6126 JValue::Number(n) => Ok(JValue::Number(-n)),
6127 _ => Err(EvaluatorError::TypeError(
6128 "D1002: Cannot negate non-number value".to_string(),
6129 )),
6130 },
6131 UnaryOp::Not => Ok(JValue::Bool(!self.is_truthy(&value))),
6132 }
6133 }
6134
6135 /// Try to fuse an aggregate function call with its Path argument.
6136 /// Handles patterns like:
6137 /// - $sum(arr.field) → iterate arr, extract field, accumulate
6138 /// - $sum(arr[pred].field) → iterate arr, filter, extract, accumulate
6139 ///
6140 /// Returns None if the pattern doesn't match (falls back to normal evaluation).
6141 fn try_fused_aggregate(
6142 &mut self,
6143 name: &str,
6144 arg: &AstNode,
6145 data: &JValue,
6146 ) -> Result<Option<JValue>, EvaluatorError> {
6147 // Only applies to numeric aggregates
6148 if !matches!(name, "sum" | "max" | "min" | "average") {
6149 return Ok(None);
6150 }
6151
6152 // Argument must be a Path
6153 let AstNode::Path { steps } = arg else {
6154 return Ok(None);
6155 };
6156
6157 // Pattern: Name(arr).Name(field) — extract field from array, aggregate
6158 // Pattern: Name(arr)[filter].Name(field) — filter, extract, aggregate
6159 if steps.len() != 2 {
6160 return Ok(None);
6161 }
6162
6163 // Last step must be a simple Name (the field to extract)
6164 let field_step = &steps[1];
6165 if !field_step.stages.is_empty() {
6166 return Ok(None);
6167 }
6168 let AstNode::Name(extract_field) = &field_step.node else {
6169 return Ok(None);
6170 };
6171
6172 // First step: Name with optional filter stage
6173 let arr_step = &steps[0];
6174 let AstNode::Name(arr_name) = &arr_step.node else {
6175 return Ok(None);
6176 };
6177
6178 // Get the source array from data
6179 let arr = match data {
6180 JValue::Object(obj) => match obj.get(arr_name) {
6181 Some(JValue::Array(arr)) => arr,
6182 _ => return Ok(None),
6183 },
6184 _ => return Ok(None),
6185 };
6186
6187 // Check for filter stage — try CompiledExpr for the predicate
6188 let filter_compiled = match arr_step.stages.as_slice() {
6189 [] => None,
6190 [Stage::Filter(pred)] => try_compile_expr(pred),
6191 _ => return Ok(None),
6192 };
6193 // If filter stage exists but wasn't compilable, bail out
6194 if !arr_step.stages.is_empty() && filter_compiled.is_none() {
6195 return Ok(None);
6196 }
6197
6198 // Build shape cache for the array
6199 let shape = arr.first().and_then(build_shape_cache);
6200
6201 // Fused iteration: filter (optional) + extract + aggregate
6202 let mut total = 0.0f64;
6203 let mut count = 0usize;
6204 let mut max_val = f64::NEG_INFINITY;
6205 let mut min_val = f64::INFINITY;
6206 let mut has_any = false;
6207
6208 for item in arr.iter() {
6209 // Apply compiled filter if present
6210 if let Some(ref compiled) = filter_compiled {
6211 let result = if let Some(ref s) = shape {
6212 eval_compiled_shaped(compiled, item, None, s)?
6213 } else {
6214 eval_compiled(compiled, item, None)?
6215 };
6216 if !compiled_is_truthy(&result) {
6217 continue;
6218 }
6219 }
6220
6221 // Extract field value
6222 let val = match item {
6223 JValue::Object(obj) => match obj.get(extract_field) {
6224 Some(JValue::Number(n)) => *n,
6225 Some(_) | None => continue, // Skip non-numeric / missing
6226 },
6227 _ => continue,
6228 };
6229
6230 has_any = true;
6231 match name {
6232 "sum" => total += val,
6233 "max" => max_val = max_val.max(val),
6234 "min" => min_val = min_val.min(val),
6235 "average" => {
6236 total += val;
6237 count += 1;
6238 }
6239 _ => unreachable!(),
6240 }
6241 }
6242
6243 if !has_any {
6244 return Ok(Some(match name {
6245 "sum" => JValue::from(0i64),
6246 "average" | "max" | "min" => JValue::Null,
6247 _ => unreachable!(),
6248 }));
6249 }
6250
6251 Ok(Some(match name {
6252 "sum" => JValue::Number(total),
6253 "max" => JValue::Number(max_val),
6254 "min" => JValue::Number(min_val),
6255 "average" => JValue::Number(total / count as f64),
6256 _ => unreachable!(),
6257 }))
6258 }
6259
6260 /// Evaluate a function call
6261 fn evaluate_function_call(
6262 &mut self,
6263 name: &str,
6264 args: &[AstNode],
6265 is_builtin: bool,
6266 data: &JValue,
6267 ) -> Result<JValue, EvaluatorError> {
6268 use crate::functions;
6269
6270 // Check for partial application (any argument is a Placeholder)
6271 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
6272 if has_placeholder {
6273 return self.create_partial_application(name, args, is_builtin, data);
6274 }
6275
6276 // FIRST check if this variable holds a function value (lambda or builtin reference)
6277 // This is critical for:
6278 // 1. Allowing function parameters to shadow stored lambdas
6279 // (e.g., Y-combinator pattern: function($g){$g($g)} where parameter $g shadows outer $g)
6280 // 2. Calling built-in functions passed as parameters
6281 // (e.g., λ($f){$f(5)}($sum) where $f is bound to $sum reference)
6282 if let Some(value) = self.context.lookup(name).cloned() {
6283 if let Some(stored_lambda) = self.lookup_lambda_from_value(&value) {
6284 let mut evaluated_args = Vec::with_capacity(args.len());
6285 for arg in args {
6286 evaluated_args.push(self.evaluate_internal(arg, data)?);
6287 }
6288 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6289 }
6290 if let JValue::Builtin { name: builtin_name } = &value {
6291 // This is a built-in function reference (e.g., $f bound to $sum)
6292 let mut evaluated_args = Vec::with_capacity(args.len());
6293 for arg in args {
6294 evaluated_args.push(self.evaluate_internal(arg, data)?);
6295 }
6296 return self.call_builtin_with_values(builtin_name, &evaluated_args);
6297 }
6298 }
6299
6300 // THEN check if this is a stored lambda (user-defined function by name)
6301 // This only applies if not shadowed by a binding above
6302 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
6303 let mut evaluated_args = Vec::with_capacity(args.len());
6304 for arg in args {
6305 evaluated_args.push(self.evaluate_internal(arg, data)?);
6306 }
6307 return self.invoke_stored_lambda(&stored_lambda, &evaluated_args, data);
6308 }
6309
6310 // If the function was called without $ prefix and it's not a stored lambda,
6311 // it's an error (unknown function without $ prefix)
6312 if !is_builtin && name != "__lambda__" {
6313 return Err(EvaluatorError::ReferenceError(format!(
6314 "Unknown function: {}",
6315 name
6316 )));
6317 }
6318
6319 // Special handling for $exists function
6320 // It needs to know if the argument is explicit null vs undefined
6321 if name == "exists" && args.len() == 1 {
6322 let arg = &args[0];
6323
6324 // Check if it's an explicit null literal
6325 if matches!(arg, AstNode::Null) {
6326 return Ok(JValue::Bool(true)); // Explicit null exists
6327 }
6328
6329 // Check if it's a function reference
6330 if let AstNode::Variable(var_name) = arg {
6331 if self.is_builtin_function(var_name) {
6332 return Ok(JValue::Bool(true)); // Built-in function exists
6333 }
6334
6335 // Check if it's a stored lambda
6336 if self.context.lookup_lambda(var_name).is_some() {
6337 return Ok(JValue::Bool(true)); // Lambda exists
6338 }
6339
6340 // Check if the variable is defined
6341 if let Some(val) = self.context.lookup(var_name) {
6342 // A variable bound to the undefined marker doesn't "exist"
6343 if val.is_undefined() {
6344 return Ok(JValue::Bool(false));
6345 }
6346 return Ok(JValue::Bool(true)); // Variable is defined (even if null)
6347 } else {
6348 return Ok(JValue::Bool(false)); // Variable is undefined
6349 }
6350 }
6351
6352 // For other expressions, evaluate and check if non-null/non-undefined
6353 let value = self.evaluate_internal(arg, data)?;
6354 return Ok(JValue::Bool(!value.is_null() && !value.is_undefined()));
6355 }
6356
6357 // Check if any arguments are undefined variables or undefined paths
6358 // Functions like $not() should return undefined when given undefined values
6359 for arg in args {
6360 // Check for undefined variable (e.g., $undefined_var)
6361 if let AstNode::Variable(var_name) = arg {
6362 // Skip built-in function names - they're function references, not undefined variables
6363 if !var_name.is_empty()
6364 && !self.is_builtin_function(var_name)
6365 && self.context.lookup(var_name).is_none()
6366 {
6367 // Undefined variable - for functions that should propagate undefined
6368 if propagates_undefined(name) {
6369 return Ok(JValue::Null); // Return undefined
6370 }
6371 }
6372 }
6373 // Check for simple field name (e.g., blah) that evaluates to undefined
6374 if let AstNode::Name(field_name) = arg {
6375 let field_exists =
6376 matches!(data, JValue::Object(obj) if obj.contains_key(field_name));
6377 if !field_exists && propagates_undefined(name) {
6378 return Ok(JValue::Null);
6379 }
6380 }
6381 // Note: AstNode::String represents string literals (e.g., "hello"), not field accesses.
6382 // Field accesses are represented as AstNode::Path. String literals should never
6383 // be checked for undefined propagation.
6384 // Check for Path expressions that evaluate to undefined
6385 if let AstNode::Path { steps } = arg {
6386 // For paths that evaluate to null, we need to determine if it's because:
6387 // 1. A field doesn't exist (undefined) - should propagate as undefined
6388 // 2. A field exists with value null - should throw T0410
6389 //
6390 // We can distinguish these by checking if the path is accessing a field
6391 // that doesn't exist on an object vs one that has an explicit null value.
6392 if let Ok(JValue::Null) = self.evaluate_internal(arg, data) {
6393 // Path evaluated to null - now check if it's truly undefined
6394 // For single-step paths, check if the field exists
6395 if steps.len() == 1 {
6396 // Get field name - could be Name (identifier) or String (quoted)
6397 let field_name = match &steps[0].node {
6398 AstNode::Name(n) => Some(n.as_str()),
6399 AstNode::String(s) => Some(s.as_str()),
6400 _ => None,
6401 };
6402 if let Some(field) = field_name {
6403 match data {
6404 JValue::Object(obj) => {
6405 if !obj.contains_key(field) {
6406 // Field doesn't exist - return undefined
6407 if propagates_undefined(name) {
6408 return Ok(JValue::Null);
6409 }
6410 }
6411 // Field exists with value null - continue to throw T0410
6412 }
6413 // Trying to access field on null data - return undefined
6414 JValue::Null if propagates_undefined(name) => {
6415 return Ok(JValue::Null);
6416 }
6417 _ => {}
6418 }
6419 }
6420 }
6421 // For multi-step paths, check if any intermediate step failed
6422 else if steps.len() > 1 {
6423 // Evaluate each step to find where it breaks
6424 let mut current = data;
6425 let mut failed_due_to_missing_field = false;
6426
6427 for (i, step) in steps.iter().enumerate() {
6428 if let AstNode::Name(field_name) = &step.node {
6429 match current {
6430 JValue::Object(obj) => {
6431 if let Some(val) = obj.get(field_name) {
6432 current = val;
6433 } else {
6434 // Field doesn't exist
6435 failed_due_to_missing_field = true;
6436 break;
6437 }
6438 }
6439 JValue::Array(_) => {
6440 // Array access - evaluate normally
6441 break;
6442 }
6443 JValue::Null => {
6444 // Hit null in the middle of the path
6445 if i > 0 {
6446 // Previous field had null value - not undefined
6447 failed_due_to_missing_field = false;
6448 }
6449 break;
6450 }
6451 _ => break,
6452 }
6453 }
6454 }
6455
6456 if failed_due_to_missing_field && propagates_undefined(name) {
6457 return Ok(JValue::Null);
6458 }
6459 }
6460 }
6461 }
6462 }
6463
6464 // Fused aggregate pipeline: for $sum/$max/$min/$average with a single Path argument,
6465 // try to fuse filter+extract+aggregate into a single pass.
6466 if args.len() == 1 {
6467 if let Some(result) = self.try_fused_aggregate(name, &args[0], data)? {
6468 return Ok(result);
6469 }
6470 }
6471
6472 let mut evaluated_args = Vec::with_capacity(args.len());
6473 for arg in args {
6474 evaluated_args.push(self.evaluate_internal(arg, data)?);
6475 }
6476
6477 // JSONata feature: when a function is called with no arguments but expects
6478 // at least one, use the current context value (data) as the implicit first argument
6479 // This also applies when functions expecting N arguments receive N-1 arguments,
6480 // in which case the context value becomes the first argument
6481 let context_functions_zero_arg = [
6482 "string",
6483 "number",
6484 "boolean",
6485 "uppercase",
6486 "lowercase",
6487 "fromMillis",
6488 ];
6489 let context_functions_missing_first = [
6490 "substringBefore",
6491 "substringAfter",
6492 "contains",
6493 "split",
6494 "replace",
6495 ];
6496
6497 if evaluated_args.is_empty() && context_functions_zero_arg.contains(&name) {
6498 // Use the current context value as the implicit argument
6499 evaluated_args.push(data.clone());
6500 } else if evaluated_args.len() == 1 && context_functions_missing_first.contains(&name) {
6501 // These functions expect 2+ arguments, but received 1
6502 // Only insert context if it's a compatible type (string for string functions)
6503 // Otherwise, let the function throw T0411 for wrong argument count
6504 if matches!(data, JValue::String(_)) {
6505 evaluated_args.insert(0, data.clone());
6506 }
6507 }
6508
6509 // Special handling for $string() with no explicit arguments
6510 // After context insertion, check if the argument is null (undefined context)
6511 if name == "string"
6512 && args.is_empty()
6513 && !evaluated_args.is_empty()
6514 && evaluated_args[0].is_null()
6515 {
6516 // Context was null/undefined, so return undefined
6517 return Ok(JValue::Null);
6518 }
6519
6520 match name {
6521 "string" => {
6522 if evaluated_args.len() > 2 {
6523 return Err(EvaluatorError::EvaluationError(
6524 "string() takes at most 2 arguments".to_string(),
6525 ));
6526 }
6527
6528 let prettify = if evaluated_args.len() == 2 {
6529 match &evaluated_args[1] {
6530 JValue::Bool(b) => Some(*b),
6531 _ => {
6532 return Err(EvaluatorError::TypeError(
6533 "string() prettify parameter must be a boolean".to_string(),
6534 ))
6535 }
6536 }
6537 } else {
6538 None
6539 };
6540
6541 Ok(functions::string::string(&evaluated_args[0], prettify)?)
6542 }
6543 "length" => {
6544 if evaluated_args.len() != 1 {
6545 return Err(EvaluatorError::EvaluationError(
6546 "length() requires exactly 1 argument".to_string(),
6547 ));
6548 }
6549 match &evaluated_args[0] {
6550 JValue::String(s) => Ok(functions::string::length(s)?),
6551 _ => Err(EvaluatorError::TypeError(
6552 "T0410: Argument 1 of function length does not match function signature"
6553 .to_string(),
6554 )),
6555 }
6556 }
6557 "uppercase" => {
6558 if evaluated_args.len() != 1 {
6559 return Err(EvaluatorError::EvaluationError(
6560 "uppercase() requires exactly 1 argument".to_string(),
6561 ));
6562 }
6563 if evaluated_args[0].is_undefined() {
6564 return Ok(JValue::Undefined);
6565 }
6566 match &evaluated_args[0] {
6567 JValue::String(s) => Ok(functions::string::uppercase(s)?),
6568 _ => Err(EvaluatorError::TypeError(
6569 "T0410: Argument 1 of function uppercase does not match function signature"
6570 .to_string(),
6571 )),
6572 }
6573 }
6574 "lowercase" => {
6575 if evaluated_args.len() != 1 {
6576 return Err(EvaluatorError::EvaluationError(
6577 "lowercase() requires exactly 1 argument".to_string(),
6578 ));
6579 }
6580 if evaluated_args[0].is_undefined() {
6581 return Ok(JValue::Undefined);
6582 }
6583 match &evaluated_args[0] {
6584 JValue::String(s) => Ok(functions::string::lowercase(s)?),
6585 _ => Err(EvaluatorError::TypeError(
6586 "T0410: Argument 1 of function lowercase does not match function signature"
6587 .to_string(),
6588 )),
6589 }
6590 }
6591 "number" => {
6592 if evaluated_args.is_empty() {
6593 return Err(EvaluatorError::EvaluationError(
6594 "number() requires at least 1 argument".to_string(),
6595 ));
6596 }
6597 if evaluated_args.len() > 1 {
6598 return Err(EvaluatorError::TypeError(
6599 "T0410: Argument 2 of function number does not match function signature"
6600 .to_string(),
6601 ));
6602 }
6603 if evaluated_args[0].is_undefined() {
6604 return Ok(JValue::Undefined);
6605 }
6606 Ok(functions::numeric::number(&evaluated_args[0])?)
6607 }
6608 "sum" => {
6609 if evaluated_args.len() != 1 {
6610 return Err(EvaluatorError::EvaluationError(
6611 "sum() requires exactly 1 argument".to_string(),
6612 ));
6613 }
6614 // Return undefined if argument is undefined
6615 if evaluated_args[0].is_undefined() {
6616 return Ok(JValue::Undefined);
6617 }
6618 match &evaluated_args[0] {
6619 JValue::Null => Ok(JValue::Null),
6620 JValue::Array(arr) => {
6621 // Use zero-clone iterator-based sum
6622 Ok(aggregation::sum(arr)?)
6623 }
6624 // Non-array values: extract number directly
6625 JValue::Number(n) => Ok(JValue::Number(*n)),
6626 other => Ok(functions::numeric::sum(&[other.clone()])?),
6627 }
6628 }
6629 "count" => {
6630 if evaluated_args.len() != 1 {
6631 return Err(EvaluatorError::EvaluationError(
6632 "count() requires exactly 1 argument".to_string(),
6633 ));
6634 }
6635 // Return 0 if argument is undefined
6636 if evaluated_args[0].is_undefined() {
6637 return Ok(JValue::from(0i64));
6638 }
6639 match &evaluated_args[0] {
6640 JValue::Null => Ok(JValue::from(0i64)), // null counts as 0
6641 JValue::Array(arr) => Ok(functions::array::count(arr)?),
6642 _ => Ok(JValue::from(1i64)), // Non-array value counts as 1
6643 }
6644 }
6645 "substring" => {
6646 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6647 return Err(EvaluatorError::EvaluationError(
6648 "substring() requires 2 or 3 arguments".to_string(),
6649 ));
6650 }
6651 if evaluated_args[0].is_undefined() {
6652 return Ok(JValue::Undefined);
6653 }
6654 match (&evaluated_args[0], &evaluated_args[1]) {
6655 (JValue::String(s), JValue::Number(start)) => {
6656 let length = if evaluated_args.len() == 3 {
6657 match &evaluated_args[2] {
6658 JValue::Number(l) => Some(*l as i64),
6659 _ => return Err(EvaluatorError::TypeError(
6660 "T0410: Argument 3 of function substring does not match function signature".to_string(),
6661 )),
6662 }
6663 } else {
6664 None
6665 };
6666 Ok(functions::string::substring(s, *start as i64, length)?)
6667 }
6668 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6669 "T0410: Argument 2 of function substring does not match function signature"
6670 .to_string(),
6671 )),
6672 _ => Err(EvaluatorError::TypeError(
6673 "T0410: Argument 1 of function substring does not match function signature"
6674 .to_string(),
6675 )),
6676 }
6677 }
6678 "substringBefore" => {
6679 if evaluated_args.len() != 2 {
6680 return Err(EvaluatorError::TypeError(
6681 "T0411: Context value is not a compatible type with argument 2 of function substringBefore".to_string(),
6682 ));
6683 }
6684 if evaluated_args[0].is_undefined() {
6685 return Ok(JValue::Undefined);
6686 }
6687 match (&evaluated_args[0], &evaluated_args[1]) {
6688 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_before(s, sep)?),
6689 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6690 "T0410: Argument 2 of function substringBefore does not match function signature".to_string(),
6691 )),
6692 _ => Err(EvaluatorError::TypeError(
6693 "T0410: Argument 1 of function substringBefore does not match function signature".to_string(),
6694 )),
6695 }
6696 }
6697 "substringAfter" => {
6698 if evaluated_args.len() != 2 {
6699 return Err(EvaluatorError::TypeError(
6700 "T0411: Context value is not a compatible type with argument 2 of function substringAfter".to_string(),
6701 ));
6702 }
6703 if evaluated_args[0].is_undefined() {
6704 return Ok(JValue::Undefined);
6705 }
6706 match (&evaluated_args[0], &evaluated_args[1]) {
6707 (JValue::String(s), JValue::String(sep)) => Ok(functions::string::substring_after(s, sep)?),
6708 (JValue::String(_), _) => Err(EvaluatorError::TypeError(
6709 "T0410: Argument 2 of function substringAfter does not match function signature".to_string(),
6710 )),
6711 _ => Err(EvaluatorError::TypeError(
6712 "T0410: Argument 1 of function substringAfter does not match function signature".to_string(),
6713 )),
6714 }
6715 }
6716 "pad" => {
6717 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
6718 return Err(EvaluatorError::EvaluationError(
6719 "pad() requires 2 or 3 arguments".to_string(),
6720 ));
6721 }
6722
6723 // First argument: string to pad
6724 let string = match &evaluated_args[0] {
6725 JValue::String(s) => s.clone(),
6726 JValue::Null => return Ok(JValue::Null),
6727 JValue::Undefined => return Ok(JValue::Undefined),
6728 _ => {
6729 return Err(EvaluatorError::TypeError(
6730 "pad() first argument must be a string".to_string(),
6731 ))
6732 }
6733 };
6734
6735 // Second argument: width (negative = left pad, positive = right pad)
6736 let width = match &evaluated_args.get(1) {
6737 Some(JValue::Number(n)) => *n as i32,
6738 _ => {
6739 return Err(EvaluatorError::TypeError(
6740 "pad() second argument must be a number".to_string(),
6741 ))
6742 }
6743 };
6744
6745 // Third argument: padding string (optional, defaults to space)
6746 let pad_string = match evaluated_args.get(2) {
6747 Some(JValue::String(s)) if !s.is_empty() => s.clone(),
6748 _ => Rc::from(" "),
6749 };
6750
6751 let abs_width = width.unsigned_abs() as usize;
6752 // Count Unicode characters (code points), not bytes
6753 let char_count = string.chars().count();
6754
6755 if char_count >= abs_width {
6756 // String is already long enough
6757 return Ok(JValue::string(string));
6758 }
6759
6760 let padding_needed = abs_width - char_count;
6761
6762 let pad_chars: Vec<char> = pad_string.chars().collect();
6763 let mut padding = String::with_capacity(padding_needed);
6764 for i in 0..padding_needed {
6765 padding.push(pad_chars[i % pad_chars.len()]);
6766 }
6767
6768 let result = if width < 0 {
6769 // Left pad (negative width)
6770 format!("{}{}", padding, string)
6771 } else {
6772 // Right pad (positive width)
6773 format!("{}{}", string, padding)
6774 };
6775
6776 Ok(JValue::string(result))
6777 }
6778
6779 "trim" => {
6780 if evaluated_args.is_empty() {
6781 return Ok(JValue::Null); // undefined
6782 }
6783 if evaluated_args.len() != 1 {
6784 return Err(EvaluatorError::EvaluationError(
6785 "trim() requires at most 1 argument".to_string(),
6786 ));
6787 }
6788 match &evaluated_args[0] {
6789 JValue::Null => Ok(JValue::Null),
6790 JValue::String(s) => Ok(functions::string::trim(s)?),
6791 _ => Err(EvaluatorError::TypeError(
6792 "trim() requires a string argument".to_string(),
6793 )),
6794 }
6795 }
6796 "contains" => {
6797 if evaluated_args.len() != 2 {
6798 return Err(EvaluatorError::EvaluationError(
6799 "contains() requires exactly 2 arguments".to_string(),
6800 ));
6801 }
6802 if evaluated_args[0].is_null() {
6803 return Ok(JValue::Null);
6804 }
6805 if evaluated_args[0].is_undefined() {
6806 return Ok(JValue::Undefined);
6807 }
6808 match &evaluated_args[0] {
6809 JValue::String(s) => Ok(functions::string::contains(s, &evaluated_args[1])?),
6810 _ => Err(EvaluatorError::TypeError(
6811 "contains() requires a string as the first argument".to_string(),
6812 )),
6813 }
6814 }
6815 "split" => {
6816 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
6817 return Err(EvaluatorError::EvaluationError(
6818 "split() requires 2 or 3 arguments".to_string(),
6819 ));
6820 }
6821 if evaluated_args[0].is_null() {
6822 return Ok(JValue::Null);
6823 }
6824 if evaluated_args[0].is_undefined() {
6825 return Ok(JValue::Undefined);
6826 }
6827 match &evaluated_args[0] {
6828 JValue::String(s) => {
6829 let limit = if evaluated_args.len() == 3 {
6830 match &evaluated_args[2] {
6831 JValue::Number(n) => {
6832 let f = *n;
6833 // Negative limit is an error
6834 if f < 0.0 {
6835 return Err(EvaluatorError::EvaluationError(
6836 "D3020: Third argument of split function must be a positive number".to_string(),
6837 ));
6838 }
6839 // Floor the value for non-integer limits
6840 Some(f.floor() as usize)
6841 }
6842 _ => {
6843 return Err(EvaluatorError::TypeError(
6844 "split() limit must be a number".to_string(),
6845 ))
6846 }
6847 }
6848 } else {
6849 None
6850 };
6851 Ok(functions::string::split(s, &evaluated_args[1], limit)?)
6852 }
6853 _ => Err(EvaluatorError::TypeError(
6854 "split() requires a string as the first argument".to_string(),
6855 )),
6856 }
6857 }
6858 "join" => {
6859 // Special case: if first arg is undefined, return undefined
6860 // But if separator (2nd arg) is undefined, use empty string (default)
6861 if evaluated_args.is_empty() {
6862 return Err(EvaluatorError::TypeError(
6863 "T0410: Argument 1 of function $join does not match function signature"
6864 .to_string(),
6865 ));
6866 }
6867 if evaluated_args[0].is_null() {
6868 return Ok(JValue::Null);
6869 }
6870 if evaluated_args[0].is_undefined() {
6871 return Ok(JValue::Undefined);
6872 }
6873
6874 // Signature: <a<s>s?:s> - array of strings, optional separator, returns string
6875 // The signature handles coercion and validation
6876 use crate::signature::Signature;
6877
6878 let signature = Signature::parse("<a<s>s?:s>").map_err(|e| {
6879 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
6880 })?;
6881
6882 let coerced_args = match signature.validate_and_coerce(&evaluated_args, data) {
6883 Ok(args) => args,
6884 Err(crate::signature::SignatureError::UndefinedArgument) => {
6885 // This can happen if the separator is undefined
6886 // In that case, just validate the first arg and use default separator
6887 let sig_first_arg = Signature::parse("<a<s>:a<s>>").map_err(|e| {
6888 EvaluatorError::EvaluationError(format!("Invalid signature: {}", e))
6889 })?;
6890
6891 match sig_first_arg.validate_and_coerce(&evaluated_args[0..1], data) {
6892 Ok(args) => args,
6893 Err(crate::signature::SignatureError::ArrayTypeMismatch {
6894 index,
6895 expected,
6896 }) => {
6897 return Err(EvaluatorError::TypeError(format!(
6898 "T0412: Argument {} of function $join must be an array of {}",
6899 index, expected
6900 )));
6901 }
6902 Err(e) => {
6903 return Err(EvaluatorError::TypeError(format!(
6904 "Signature validation failed: {}",
6905 e
6906 )));
6907 }
6908 }
6909 }
6910 Err(crate::signature::SignatureError::ArgumentTypeMismatch {
6911 index,
6912 expected,
6913 }) => {
6914 return Err(EvaluatorError::TypeError(
6915 format!("T0410: Argument {} of function $join does not match function signature (expected {})", index, expected)
6916 ));
6917 }
6918 Err(crate::signature::SignatureError::ArrayTypeMismatch {
6919 index,
6920 expected,
6921 }) => {
6922 return Err(EvaluatorError::TypeError(format!(
6923 "T0412: Argument {} of function $join must be an array of {}",
6924 index, expected
6925 )));
6926 }
6927 Err(e) => {
6928 return Err(EvaluatorError::TypeError(format!(
6929 "Signature validation failed: {}",
6930 e
6931 )));
6932 }
6933 };
6934
6935 // After coercion, first arg is guaranteed to be an array of strings
6936 match &coerced_args[0] {
6937 JValue::Array(arr) => {
6938 let separator = if coerced_args.len() == 2 {
6939 match &coerced_args[1] {
6940 JValue::String(s) => Some(&**s),
6941 JValue::Null => None, // Undefined separator -> use empty string
6942 _ => None, // Signature should have validated this
6943 }
6944 } else {
6945 None // No separator provided -> use empty string
6946 };
6947 Ok(functions::string::join(arr, separator)?)
6948 }
6949 JValue::Null => Ok(JValue::Null),
6950 _ => unreachable!("Signature validation should ensure array type"),
6951 }
6952 }
6953 "replace" => {
6954 if evaluated_args.len() < 3 || evaluated_args.len() > 4 {
6955 return Err(EvaluatorError::EvaluationError(
6956 "replace() requires 3 or 4 arguments".to_string(),
6957 ));
6958 }
6959 if evaluated_args[0].is_null() {
6960 return Ok(JValue::Null);
6961 }
6962 if evaluated_args[0].is_undefined() {
6963 return Ok(JValue::Undefined);
6964 }
6965
6966 // Check if replacement (3rd arg) is a function/lambda
6967 let replacement_is_lambda = matches!(
6968 evaluated_args[2],
6969 JValue::Lambda { .. } | JValue::Builtin { .. }
6970 );
6971
6972 if replacement_is_lambda {
6973 // Lambda replacement mode
6974 return self.replace_with_lambda(
6975 &evaluated_args[0],
6976 &evaluated_args[1],
6977 &evaluated_args[2],
6978 if evaluated_args.len() == 4 {
6979 Some(&evaluated_args[3])
6980 } else {
6981 None
6982 },
6983 data,
6984 );
6985 }
6986
6987 // String replacement mode
6988 match (&evaluated_args[0], &evaluated_args[2]) {
6989 (JValue::String(s), JValue::String(replacement)) => {
6990 let limit = if evaluated_args.len() == 4 {
6991 match &evaluated_args[3] {
6992 JValue::Number(n) => {
6993 let lim_f64 = *n;
6994 if lim_f64 < 0.0 {
6995 return Err(EvaluatorError::EvaluationError(format!(
6996 "D3011: Limit must be non-negative, got {}",
6997 lim_f64
6998 )));
6999 }
7000 Some(lim_f64 as usize)
7001 }
7002 _ => {
7003 return Err(EvaluatorError::TypeError(
7004 "replace() limit must be a number".to_string(),
7005 ))
7006 }
7007 }
7008 } else {
7009 None
7010 };
7011 Ok(functions::string::replace(
7012 s,
7013 &evaluated_args[1],
7014 replacement,
7015 limit,
7016 )?)
7017 }
7018 _ => Err(EvaluatorError::TypeError(
7019 "replace() requires string arguments".to_string(),
7020 )),
7021 }
7022 }
7023 "match" => {
7024 // $match(str, pattern [, limit])
7025 // Returns array of match objects for regex matches or custom matcher function
7026 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
7027 return Err(EvaluatorError::EvaluationError(
7028 "match() requires 1 to 3 arguments".to_string(),
7029 ));
7030 }
7031 if evaluated_args[0].is_null() {
7032 return Ok(JValue::Null);
7033 }
7034 if evaluated_args[0].is_undefined() {
7035 return Ok(JValue::Undefined);
7036 }
7037
7038 let s = match &evaluated_args[0] {
7039 JValue::String(s) => s.clone(),
7040 _ => {
7041 return Err(EvaluatorError::TypeError(
7042 "match() first argument must be a string".to_string(),
7043 ))
7044 }
7045 };
7046
7047 // Get optional limit
7048 let limit = if evaluated_args.len() == 3 {
7049 match &evaluated_args[2] {
7050 JValue::Number(n) => Some(*n as usize),
7051 JValue::Null => None,
7052 _ => {
7053 return Err(EvaluatorError::TypeError(
7054 "match() limit must be a number".to_string(),
7055 ))
7056 }
7057 }
7058 } else {
7059 None
7060 };
7061
7062 // Check if second argument is a custom matcher function (lambda)
7063 let pattern_value = evaluated_args.get(1);
7064 let is_custom_matcher = pattern_value.is_some_and(|val| {
7065 matches!(val, JValue::Lambda { .. } | JValue::Builtin { .. })
7066 });
7067
7068 if is_custom_matcher {
7069 // Custom matcher function support
7070 // Call the matcher with the string, get match objects with {match, start, end, groups, next}
7071 return self.match_with_custom_matcher(&s, &args[1], limit, data);
7072 }
7073
7074 // Get regex pattern from second argument
7075 let (pattern, flags) = match pattern_value {
7076 Some(val) => crate::functions::string::extract_regex(val).ok_or_else(|| {
7077 EvaluatorError::TypeError(
7078 "match() second argument must be a regex pattern or matcher function"
7079 .to_string(),
7080 )
7081 })?,
7082 None => (".*".to_string(), "".to_string()),
7083 };
7084
7085 // Build regex
7086 let is_global = flags.contains('g');
7087 let regex_pattern = if flags.contains('i') {
7088 format!("(?i){}", pattern)
7089 } else {
7090 pattern.clone()
7091 };
7092
7093 let re = regex::Regex::new(®ex_pattern).map_err(|e| {
7094 EvaluatorError::EvaluationError(format!("Invalid regex pattern: {}", e))
7095 })?;
7096
7097 let mut results = Vec::new();
7098 let mut count = 0;
7099
7100 for caps in re.captures_iter(&s) {
7101 if let Some(lim) = limit {
7102 if count >= lim {
7103 break;
7104 }
7105 }
7106
7107 let full_match = caps.get(0).unwrap();
7108 let mut match_obj = IndexMap::new();
7109 match_obj.insert(
7110 "match".to_string(),
7111 JValue::string(full_match.as_str().to_string()),
7112 );
7113 match_obj.insert(
7114 "index".to_string(),
7115 JValue::Number(full_match.start() as f64),
7116 );
7117
7118 // Collect capture groups
7119 let mut groups: Vec<JValue> = Vec::new();
7120 for i in 1..caps.len() {
7121 if let Some(group) = caps.get(i) {
7122 groups.push(JValue::string(group.as_str().to_string()));
7123 } else {
7124 groups.push(JValue::Null);
7125 }
7126 }
7127 if !groups.is_empty() {
7128 match_obj.insert("groups".to_string(), JValue::array(groups));
7129 }
7130
7131 results.push(JValue::object(match_obj));
7132 count += 1;
7133
7134 // If not global, only return first match
7135 if !is_global {
7136 break;
7137 }
7138 }
7139
7140 if results.is_empty() {
7141 Ok(JValue::Null)
7142 } else if results.len() == 1 && !is_global {
7143 // Single match (non-global) returns the match object directly
7144 Ok(results.into_iter().next().unwrap())
7145 } else {
7146 Ok(JValue::array(results))
7147 }
7148 }
7149 "max" => {
7150 if evaluated_args.len() != 1 {
7151 return Err(EvaluatorError::EvaluationError(
7152 "max() requires exactly 1 argument".to_string(),
7153 ));
7154 }
7155 // Check for undefined
7156 if evaluated_args[0].is_undefined() {
7157 return Ok(JValue::Undefined);
7158 }
7159 match &evaluated_args[0] {
7160 JValue::Null => Ok(JValue::Null),
7161 JValue::Array(arr) => {
7162 // Use zero-clone iterator-based max
7163 Ok(aggregation::max(arr)?)
7164 }
7165 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7166 _ => Err(EvaluatorError::TypeError(
7167 "max() requires an array or number argument".to_string(),
7168 )),
7169 }
7170 }
7171 "min" => {
7172 if evaluated_args.len() != 1 {
7173 return Err(EvaluatorError::EvaluationError(
7174 "min() requires exactly 1 argument".to_string(),
7175 ));
7176 }
7177 // Check for undefined
7178 if evaluated_args[0].is_undefined() {
7179 return Ok(JValue::Undefined);
7180 }
7181 match &evaluated_args[0] {
7182 JValue::Null => Ok(JValue::Null),
7183 JValue::Array(arr) => {
7184 // Use zero-clone iterator-based min
7185 Ok(aggregation::min(arr)?)
7186 }
7187 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7188 _ => Err(EvaluatorError::TypeError(
7189 "min() requires an array or number argument".to_string(),
7190 )),
7191 }
7192 }
7193 "average" => {
7194 if evaluated_args.len() != 1 {
7195 return Err(EvaluatorError::EvaluationError(
7196 "average() requires exactly 1 argument".to_string(),
7197 ));
7198 }
7199 // Return undefined if argument is undefined
7200 if evaluated_args[0].is_undefined() {
7201 return Ok(JValue::Undefined);
7202 }
7203 match &evaluated_args[0] {
7204 JValue::Null => Ok(JValue::Null),
7205 JValue::Array(arr) => {
7206 // Use zero-clone iterator-based average
7207 Ok(aggregation::average(arr)?)
7208 }
7209 JValue::Number(_) => Ok(evaluated_args[0].clone()), // Single number returns itself
7210 _ => Err(EvaluatorError::TypeError(
7211 "average() requires an array or number argument".to_string(),
7212 )),
7213 }
7214 }
7215 "abs" => {
7216 if evaluated_args.len() != 1 {
7217 return Err(EvaluatorError::EvaluationError(
7218 "abs() requires exactly 1 argument".to_string(),
7219 ));
7220 }
7221 match &evaluated_args[0] {
7222 JValue::Null => Ok(JValue::Null),
7223 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
7224 _ => Err(EvaluatorError::TypeError(
7225 "abs() requires a number argument".to_string(),
7226 )),
7227 }
7228 }
7229 "floor" => {
7230 if evaluated_args.len() != 1 {
7231 return Err(EvaluatorError::EvaluationError(
7232 "floor() requires exactly 1 argument".to_string(),
7233 ));
7234 }
7235 match &evaluated_args[0] {
7236 JValue::Null => Ok(JValue::Null),
7237 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
7238 _ => Err(EvaluatorError::TypeError(
7239 "floor() requires a number argument".to_string(),
7240 )),
7241 }
7242 }
7243 "ceil" => {
7244 if evaluated_args.len() != 1 {
7245 return Err(EvaluatorError::EvaluationError(
7246 "ceil() requires exactly 1 argument".to_string(),
7247 ));
7248 }
7249 match &evaluated_args[0] {
7250 JValue::Null => Ok(JValue::Null),
7251 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
7252 _ => Err(EvaluatorError::TypeError(
7253 "ceil() requires a number argument".to_string(),
7254 )),
7255 }
7256 }
7257 "round" => {
7258 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7259 return Err(EvaluatorError::EvaluationError(
7260 "round() requires 1 or 2 arguments".to_string(),
7261 ));
7262 }
7263 match &evaluated_args[0] {
7264 JValue::Null => Ok(JValue::Null),
7265 JValue::Number(n) => {
7266 let precision = if evaluated_args.len() == 2 {
7267 match &evaluated_args[1] {
7268 JValue::Number(p) => Some(*p as i32),
7269 _ => {
7270 return Err(EvaluatorError::TypeError(
7271 "round() precision must be a number".to_string(),
7272 ))
7273 }
7274 }
7275 } else {
7276 None
7277 };
7278 Ok(functions::numeric::round(*n, precision)?)
7279 }
7280 _ => Err(EvaluatorError::TypeError(
7281 "round() requires a number argument".to_string(),
7282 )),
7283 }
7284 }
7285 "sqrt" => {
7286 if evaluated_args.len() != 1 {
7287 return Err(EvaluatorError::EvaluationError(
7288 "sqrt() requires exactly 1 argument".to_string(),
7289 ));
7290 }
7291 match &evaluated_args[0] {
7292 JValue::Null => Ok(JValue::Null),
7293 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
7294 _ => Err(EvaluatorError::TypeError(
7295 "sqrt() requires a number argument".to_string(),
7296 )),
7297 }
7298 }
7299 "power" => {
7300 if evaluated_args.len() != 2 {
7301 return Err(EvaluatorError::EvaluationError(
7302 "power() requires exactly 2 arguments".to_string(),
7303 ));
7304 }
7305 if evaluated_args[0].is_null() {
7306 return Ok(JValue::Null);
7307 }
7308 if evaluated_args[0].is_undefined() {
7309 return Ok(JValue::Undefined);
7310 }
7311 match (&evaluated_args[0], &evaluated_args[1]) {
7312 (JValue::Number(base), JValue::Number(exp)) => {
7313 Ok(functions::numeric::power(*base, *exp)?)
7314 }
7315 _ => Err(EvaluatorError::TypeError(
7316 "power() requires number arguments".to_string(),
7317 )),
7318 }
7319 }
7320 "formatNumber" => {
7321 if evaluated_args.len() < 2 || evaluated_args.len() > 3 {
7322 return Err(EvaluatorError::EvaluationError(
7323 "formatNumber() requires 2 or 3 arguments".to_string(),
7324 ));
7325 }
7326 if evaluated_args[0].is_null() {
7327 return Ok(JValue::Null);
7328 }
7329 if evaluated_args[0].is_undefined() {
7330 return Ok(JValue::Undefined);
7331 }
7332 match (&evaluated_args[0], &evaluated_args[1]) {
7333 (JValue::Number(num), JValue::String(picture)) => {
7334 let options = if evaluated_args.len() == 3 {
7335 Some(&evaluated_args[2])
7336 } else {
7337 None
7338 };
7339 Ok(functions::numeric::format_number(*num, picture, options)?)
7340 }
7341 _ => Err(EvaluatorError::TypeError(
7342 "formatNumber() requires a number and a string".to_string(),
7343 )),
7344 }
7345 }
7346 "formatBase" => {
7347 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7348 return Err(EvaluatorError::EvaluationError(
7349 "formatBase() requires 1 or 2 arguments".to_string(),
7350 ));
7351 }
7352 // Handle undefined input
7353 if evaluated_args[0].is_null() {
7354 return Ok(JValue::Null);
7355 }
7356 if evaluated_args[0].is_undefined() {
7357 return Ok(JValue::Undefined);
7358 }
7359 match &evaluated_args[0] {
7360 JValue::Number(num) => {
7361 let radix = if evaluated_args.len() == 2 {
7362 match &evaluated_args[1] {
7363 JValue::Number(r) => Some(r.trunc() as i64),
7364 _ => {
7365 return Err(EvaluatorError::TypeError(
7366 "formatBase() radix must be a number".to_string(),
7367 ))
7368 }
7369 }
7370 } else {
7371 None
7372 };
7373 Ok(functions::numeric::format_base(*num, radix)?)
7374 }
7375 _ => Err(EvaluatorError::TypeError(
7376 "formatBase() requires a number".to_string(),
7377 )),
7378 }
7379 }
7380 "formatInteger" => {
7381 if evaluated_args.len() != 2 {
7382 return Err(EvaluatorError::EvaluationError(
7383 "formatInteger() requires exactly 2 arguments".to_string(),
7384 ));
7385 }
7386 match (&evaluated_args[0], &evaluated_args[1]) {
7387 (JValue::Number(n), JValue::String(picture)) => {
7388 Ok(crate::datetime::format_integer(*n, picture)?)
7389 }
7390 (JValue::Null, _) => Ok(JValue::Null),
7391 (JValue::Undefined, _) => Ok(JValue::Undefined),
7392 _ => Err(EvaluatorError::TypeError(
7393 "formatInteger() requires a number and a string".to_string(),
7394 )),
7395 }
7396 }
7397 "parseInteger" => {
7398 if evaluated_args.len() != 2 {
7399 return Err(EvaluatorError::EvaluationError(
7400 "parseInteger() requires exactly 2 arguments".to_string(),
7401 ));
7402 }
7403 match (&evaluated_args[0], &evaluated_args[1]) {
7404 (JValue::String(value), JValue::String(picture)) => {
7405 Ok(crate::datetime::parse_integer(value, picture)?)
7406 }
7407 (JValue::Null, _) => Ok(JValue::Null),
7408 (JValue::Undefined, _) => Ok(JValue::Undefined),
7409 _ => Err(EvaluatorError::TypeError(
7410 "parseInteger() requires a string and a string".to_string(),
7411 )),
7412 }
7413 }
7414 "append" => {
7415 if evaluated_args.len() != 2 {
7416 return Err(EvaluatorError::EvaluationError(
7417 "append() requires exactly 2 arguments".to_string(),
7418 ));
7419 }
7420 // Handle null/undefined arguments
7421 let first = &evaluated_args[0];
7422 let second = &evaluated_args[1];
7423
7424 // If second arg is null/undefined, return first as-is (no change)
7425 if second.is_null() || second.is_undefined() {
7426 return Ok(first.clone());
7427 }
7428
7429 // If first arg is null/undefined, return second as-is (appending to nothing gives second)
7430 if first.is_null() || first.is_undefined() {
7431 return Ok(second.clone());
7432 }
7433
7434 // Convert both to arrays if needed, then append
7435 let arr = match first {
7436 JValue::Array(a) => a.to_vec(),
7437 other => vec![other.clone()], // Wrap non-array in array
7438 };
7439
7440 Ok(functions::array::append(&arr, second)?)
7441 }
7442 "reverse" => {
7443 if evaluated_args.len() != 1 {
7444 return Err(EvaluatorError::EvaluationError(
7445 "reverse() requires exactly 1 argument".to_string(),
7446 ));
7447 }
7448 match &evaluated_args[0] {
7449 JValue::Null => Ok(JValue::Null),
7450 JValue::Undefined => Ok(JValue::Undefined),
7451 JValue::Array(arr) => Ok(functions::array::reverse(arr)?),
7452 _ => Err(EvaluatorError::TypeError(
7453 "reverse() requires an array argument".to_string(),
7454 )),
7455 }
7456 }
7457 "shuffle" => {
7458 if evaluated_args.len() != 1 {
7459 return Err(EvaluatorError::EvaluationError(
7460 "shuffle() requires exactly 1 argument".to_string(),
7461 ));
7462 }
7463 if evaluated_args[0].is_null() {
7464 return Ok(JValue::Null);
7465 }
7466 if evaluated_args[0].is_undefined() {
7467 return Ok(JValue::Undefined);
7468 }
7469 match &evaluated_args[0] {
7470 JValue::Array(arr) => Ok(functions::array::shuffle(arr)?),
7471 _ => Err(EvaluatorError::TypeError(
7472 "shuffle() requires an array argument".to_string(),
7473 )),
7474 }
7475 }
7476
7477 "sift" => {
7478 // $sift(object, function) or $sift(function) - filter object by predicate
7479 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7480 return Err(EvaluatorError::EvaluationError(
7481 "sift() requires 1 or 2 arguments".to_string(),
7482 ));
7483 }
7484
7485 // Determine which argument is the function
7486 let func_arg = if evaluated_args.len() == 1 {
7487 &args[0]
7488 } else {
7489 &args[1]
7490 };
7491
7492 // Detect how many parameters the callback expects
7493 let param_count = self.get_callback_param_count(func_arg);
7494
7495 // Helper function to sift a single object
7496 let sift_object = |evaluator: &mut Self,
7497 obj: &IndexMap<String, JValue>,
7498 func_node: &AstNode,
7499 context_data: &JValue,
7500 param_count: usize|
7501 -> Result<JValue, EvaluatorError> {
7502 // Only create the object value if callback uses 3 parameters
7503 let obj_value = if param_count >= 3 {
7504 Some(JValue::object(obj.clone()))
7505 } else {
7506 None
7507 };
7508
7509 let mut result = IndexMap::new();
7510 for (key, value) in obj.iter() {
7511 // Build argument list based on what callback expects
7512 let call_args = match param_count {
7513 1 => vec![value.clone()],
7514 2 => vec![value.clone(), JValue::string(key.clone())],
7515 _ => vec![
7516 value.clone(),
7517 JValue::string(key.clone()),
7518 obj_value.as_ref().unwrap().clone(),
7519 ],
7520 };
7521
7522 let pred_result =
7523 evaluator.apply_function(func_node, &call_args, context_data)?;
7524 if evaluator.is_truthy(&pred_result) {
7525 result.insert(key.clone(), value.clone());
7526 }
7527 }
7528 // Return undefined for empty results (will be filtered by function application)
7529 if result.is_empty() {
7530 Ok(JValue::Undefined)
7531 } else {
7532 Ok(JValue::object(result))
7533 }
7534 };
7535
7536 // Handle partial application - if only 1 arg, use current context as object
7537 if evaluated_args.len() == 1 {
7538 // $sift(function) - use current context data as object
7539 match data {
7540 JValue::Object(o) => sift_object(self, o, &args[0], data, param_count),
7541 JValue::Array(arr) => {
7542 // Map sift over each object in the array
7543 let mut results = Vec::new();
7544 for item in arr.iter() {
7545 if let JValue::Object(o) = item {
7546 let sifted = sift_object(self, o, &args[0], item, param_count)?;
7547 // sift_object returns undefined for empty results
7548 if !sifted.is_undefined() {
7549 results.push(sifted);
7550 }
7551 }
7552 }
7553 Ok(JValue::array(results))
7554 }
7555 JValue::Null => Ok(JValue::Null),
7556 _ => Ok(JValue::Undefined),
7557 }
7558 } else {
7559 // $sift(object, function)
7560 match &evaluated_args[0] {
7561 JValue::Object(o) => sift_object(self, o, &args[1], data, param_count),
7562 JValue::Null => Ok(JValue::Null),
7563 _ => Err(EvaluatorError::TypeError(
7564 "sift() first argument must be an object".to_string(),
7565 )),
7566 }
7567 }
7568 }
7569
7570 "zip" => {
7571 if evaluated_args.is_empty() {
7572 return Err(EvaluatorError::EvaluationError(
7573 "zip() requires at least 1 argument".to_string(),
7574 ));
7575 }
7576
7577 // Convert arguments to arrays (wrapping non-arrays in single-element arrays)
7578 // If any argument is null/undefined, return empty array
7579 let mut arrays: Vec<Vec<JValue>> = Vec::with_capacity(evaluated_args.len());
7580 for arg in &evaluated_args {
7581 match arg {
7582 JValue::Array(arr) => {
7583 if arr.is_empty() {
7584 // Empty array means result is empty
7585 return Ok(JValue::array(vec![]));
7586 }
7587 arrays.push(arr.to_vec());
7588 }
7589 JValue::Null | JValue::Undefined => {
7590 // Null/undefined means result is empty
7591 return Ok(JValue::array(vec![]));
7592 }
7593 other => {
7594 // Wrap non-array values in single-element array
7595 arrays.push(vec![other.clone()]);
7596 }
7597 }
7598 }
7599
7600 if arrays.is_empty() {
7601 return Ok(JValue::array(vec![]));
7602 }
7603
7604 // Find the length of the shortest array
7605 let min_len = arrays.iter().map(|a| a.len()).min().unwrap_or(0);
7606
7607 // Zip the arrays together
7608 let mut result = Vec::with_capacity(min_len);
7609 for i in 0..min_len {
7610 let mut tuple = Vec::with_capacity(arrays.len());
7611 for array in &arrays {
7612 tuple.push(array[i].clone());
7613 }
7614 result.push(JValue::array(tuple));
7615 }
7616
7617 Ok(JValue::array(result))
7618 }
7619
7620 "sort" => {
7621 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
7622 return Err(EvaluatorError::EvaluationError(
7623 "sort() requires 1 or 2 arguments".to_string(),
7624 ));
7625 }
7626
7627 // Use pre-evaluated first argument (avoid double evaluation)
7628 let array_value = &evaluated_args[0];
7629
7630 // Handle undefined input
7631 if array_value.is_null() {
7632 return Ok(JValue::Null);
7633 }
7634 if array_value.is_undefined() {
7635 return Ok(JValue::Undefined);
7636 }
7637
7638 let mut arr = match array_value {
7639 JValue::Array(arr) => arr.to_vec(),
7640 other => vec![other.clone()],
7641 };
7642
7643 if args.len() == 2 {
7644 // Sort using the comparator from raw args (need unevaluated lambda AST)
7645 // Use merge sort for O(n log n) performance instead of O(n²) bubble sort
7646 self.merge_sort_with_comparator(&mut arr, &args[1], data)?;
7647 Ok(JValue::array(arr))
7648 } else {
7649 // Default sort (no comparator)
7650 Ok(functions::array::sort(&arr)?)
7651 }
7652 }
7653 "distinct" => {
7654 if evaluated_args.len() != 1 {
7655 return Err(EvaluatorError::EvaluationError(
7656 "distinct() requires exactly 1 argument".to_string(),
7657 ));
7658 }
7659 match &evaluated_args[0] {
7660 JValue::Array(arr) if arr.len() > 1 => Ok(functions::array::distinct(arr)?),
7661 // Non-array input, and arrays of length <= 1, pass through
7662 // unchanged (jsonata-js functions.js:
7663 // `if(!Array.isArray(arr) || arr.length <= 1) return arr;`)
7664 other => Ok(other.clone()),
7665 }
7666 }
7667 "exists" => {
7668 if evaluated_args.len() != 1 {
7669 return Err(EvaluatorError::EvaluationError(
7670 "exists() requires exactly 1 argument".to_string(),
7671 ));
7672 }
7673 Ok(functions::array::exists(&evaluated_args[0])?)
7674 }
7675 "keys" => {
7676 if evaluated_args.len() != 1 {
7677 return Err(EvaluatorError::EvaluationError(
7678 "keys() requires exactly 1 argument".to_string(),
7679 ));
7680 }
7681
7682 // Helper to unwrap single-element arrays
7683 let unwrap_single = |keys: Vec<JValue>| -> JValue {
7684 if keys.len() == 1 {
7685 keys.into_iter().next().unwrap()
7686 } else {
7687 JValue::array(keys)
7688 }
7689 };
7690
7691 match &evaluated_args[0] {
7692 JValue::Null => Ok(JValue::Null),
7693 JValue::Lambda { .. } | JValue::Builtin { .. } => Ok(JValue::Null),
7694 JValue::Object(obj) => {
7695 // Return undefined for empty objects
7696 if obj.is_empty() {
7697 Ok(JValue::Null)
7698 } else {
7699 let keys: Vec<JValue> =
7700 obj.keys().map(|k| JValue::string(k.clone())).collect();
7701 Ok(unwrap_single(keys))
7702 }
7703 }
7704 JValue::Array(arr) => {
7705 // For arrays, collect keys from all objects
7706 let mut all_keys = Vec::new();
7707 for item in arr.iter() {
7708 // Skip lambda/builtin values
7709 if matches!(item, JValue::Lambda { .. } | JValue::Builtin { .. }) {
7710 continue;
7711 }
7712 if let JValue::Object(obj) = item {
7713 for key in obj.keys() {
7714 if !all_keys.contains(&JValue::string(key.clone())) {
7715 all_keys.push(JValue::string(key.clone()));
7716 }
7717 }
7718 }
7719 }
7720 if all_keys.is_empty() {
7721 Ok(JValue::Null)
7722 } else {
7723 Ok(unwrap_single(all_keys))
7724 }
7725 }
7726 // Non-object types return undefined
7727 _ => Ok(JValue::Null),
7728 }
7729 }
7730 "lookup" => {
7731 if evaluated_args.len() != 2 {
7732 return Err(EvaluatorError::EvaluationError(
7733 "lookup() requires exactly 2 arguments".to_string(),
7734 ));
7735 }
7736 if evaluated_args[0].is_null() {
7737 return Ok(JValue::Null);
7738 }
7739 if evaluated_args[0].is_undefined() {
7740 return Ok(JValue::Undefined);
7741 }
7742
7743 let key = match &evaluated_args[1] {
7744 JValue::String(k) => &**k,
7745 _ => {
7746 return Err(EvaluatorError::TypeError(
7747 "lookup() requires a string key".to_string(),
7748 ))
7749 }
7750 };
7751
7752 // Helper function to recursively lookup in values
7753 fn lookup_recursive(val: &JValue, key: &str) -> Vec<JValue> {
7754 match val {
7755 JValue::Array(arr) => {
7756 let mut results = Vec::new();
7757 for item in arr.iter() {
7758 let nested = lookup_recursive(item, key);
7759 results.extend(nested.iter().cloned());
7760 }
7761 results
7762 }
7763 JValue::Object(obj) => {
7764 if let Some(v) = obj.get(key) {
7765 vec![v.clone()]
7766 } else {
7767 vec![]
7768 }
7769 }
7770 _ => vec![],
7771 }
7772 }
7773
7774 let results = lookup_recursive(&evaluated_args[0], key);
7775 if results.is_empty() {
7776 Ok(JValue::Null)
7777 } else if results.len() == 1 {
7778 Ok(results[0].clone())
7779 } else {
7780 Ok(JValue::array(results))
7781 }
7782 }
7783 "spread" => {
7784 if evaluated_args.len() != 1 {
7785 return Err(EvaluatorError::EvaluationError(
7786 "spread() requires exactly 1 argument".to_string(),
7787 ));
7788 }
7789 match &evaluated_args[0] {
7790 JValue::Null => Ok(JValue::Null),
7791 // Not a container - pass through unchanged (e.g. so $string() still
7792 // sees the function value and applies its own function->"" rule).
7793 lambda @ (JValue::Lambda { .. } | JValue::Builtin { .. }) => Ok(lambda.clone()),
7794 JValue::Object(obj) => Ok(functions::object::spread(obj)?),
7795 JValue::Array(arr) => {
7796 // Spread each object in the array
7797 let mut result = Vec::new();
7798 for item in arr.iter() {
7799 match item {
7800 JValue::Lambda { .. } | JValue::Builtin { .. } => {
7801 // Skip lambdas in array
7802 continue;
7803 }
7804 JValue::Object(obj) => {
7805 let spread_result = functions::object::spread(obj)?;
7806 if let JValue::Array(spread_items) = spread_result {
7807 result.extend(spread_items.iter().cloned());
7808 } else {
7809 result.push(spread_result);
7810 }
7811 }
7812 // Non-objects in array are returned unchanged
7813 other => result.push(other.clone()),
7814 }
7815 }
7816 Ok(JValue::array(result))
7817 }
7818 // Non-objects are returned unchanged
7819 other => Ok(other.clone()),
7820 }
7821 }
7822 "merge" => {
7823 if evaluated_args.is_empty() {
7824 return Err(EvaluatorError::EvaluationError(
7825 "merge() requires at least 1 argument".to_string(),
7826 ));
7827 }
7828 // Handle the case where a single array of objects is passed: $merge([obj1, obj2])
7829 // vs multiple object arguments: $merge(obj1, obj2)
7830 if evaluated_args.len() == 1 {
7831 match &evaluated_args[0] {
7832 JValue::Array(arr) => Ok(functions::object::merge(arr)?),
7833 JValue::Null => Ok(JValue::Null),
7834 JValue::Undefined => Ok(JValue::Undefined),
7835 JValue::Object(_) => {
7836 // Single object - just return it
7837 Ok(evaluated_args[0].clone())
7838 }
7839 _ => Err(EvaluatorError::TypeError(
7840 "merge() requires objects or an array of objects".to_string(),
7841 )),
7842 }
7843 } else {
7844 Ok(functions::object::merge(&evaluated_args)?)
7845 }
7846 }
7847
7848 "map" => {
7849 if args.len() != 2 {
7850 return Err(EvaluatorError::EvaluationError(
7851 "map() requires exactly 2 arguments".to_string(),
7852 ));
7853 }
7854
7855 // Evaluate the array argument
7856 let array = self.evaluate_internal(&args[0], data)?;
7857
7858 match array {
7859 JValue::Array(arr) => {
7860 // Detect how many parameters the callback expects
7861 let param_count = self.get_callback_param_count(&args[1]);
7862
7863 // CompiledExpr fast path: direct lambda with 1 param, compilable body
7864 if param_count == 1 {
7865 if let AstNode::Lambda {
7866 params,
7867 body,
7868 signature: None,
7869 thunk: false,
7870 } = &args[1]
7871 {
7872 let var_refs: Vec<&str> =
7873 params.iter().map(|s| s.as_str()).collect();
7874 if let Some(compiled) =
7875 try_compile_expr_with_allowed_vars(body, &var_refs)
7876 {
7877 let param_name = params[0].as_str();
7878 let mut result = Vec::with_capacity(arr.len());
7879 let mut vars = HashMap::new();
7880 for item in arr.iter() {
7881 vars.insert(param_name, item);
7882 let mapped = eval_compiled(&compiled, data, Some(&vars))?;
7883 if !mapped.is_undefined() {
7884 result.push(mapped);
7885 }
7886 }
7887 return Ok(JValue::array(result));
7888 }
7889 }
7890 // Stored lambda variable fast path: $var with pre-compiled body
7891 if let AstNode::Variable(var_name) = &args[1] {
7892 if let Some(stored) = self.context.lookup_lambda(var_name) {
7893 if let Some(ref ce) = stored.compiled_body.clone() {
7894 let param_name = stored.params[0].clone();
7895 let captured_data = stored.captured_data.clone();
7896 let captured_env_clone = stored.captured_env.clone();
7897 let ce_clone = ce.clone();
7898 if !captured_env_clone.values().any(|v| {
7899 matches!(
7900 v,
7901 JValue::Lambda { .. } | JValue::Builtin { .. }
7902 )
7903 }) {
7904 let call_data = captured_data.as_ref().unwrap_or(data);
7905 let mut result = Vec::with_capacity(arr.len());
7906 let mut vars: HashMap<&str, &JValue> =
7907 captured_env_clone
7908 .iter()
7909 .map(|(k, v)| (k.as_str(), v))
7910 .collect();
7911 for item in arr.iter() {
7912 vars.insert(param_name.as_str(), item);
7913 let mapped = eval_compiled(
7914 &ce_clone,
7915 call_data,
7916 Some(&vars),
7917 )?;
7918 if !mapped.is_undefined() {
7919 result.push(mapped);
7920 }
7921 }
7922 return Ok(JValue::array(result));
7923 }
7924 }
7925 }
7926 }
7927 }
7928
7929 // Only create the array value if callback uses 3 parameters
7930 let arr_value = if param_count >= 3 {
7931 Some(JValue::Array(arr.clone()))
7932 } else {
7933 None
7934 };
7935
7936 let mut result = Vec::with_capacity(arr.len());
7937 for (index, item) in arr.iter().enumerate() {
7938 // Build argument list based on what callback expects
7939 let call_args = match param_count {
7940 1 => vec![item.clone()],
7941 2 => vec![item.clone(), JValue::Number(index as f64)],
7942 _ => vec![
7943 item.clone(),
7944 JValue::Number(index as f64),
7945 arr_value.as_ref().unwrap().clone(),
7946 ],
7947 };
7948
7949 let mapped = self.apply_function(&args[1], &call_args, data)?;
7950 // Filter out undefined results but keep explicit null (JSONata map semantics)
7951 // undefined comes from missing else clause, null is explicit
7952 if !mapped.is_undefined() {
7953 result.push(mapped);
7954 }
7955 }
7956 Ok(JValue::array(result))
7957 }
7958 JValue::Null => Ok(JValue::Null),
7959 JValue::Undefined => Ok(JValue::Undefined),
7960 _ => Err(EvaluatorError::TypeError(
7961 "map() first argument must be an array".to_string(),
7962 )),
7963 }
7964 }
7965
7966 "filter" => {
7967 if args.len() != 2 {
7968 return Err(EvaluatorError::EvaluationError(
7969 "filter() requires exactly 2 arguments".to_string(),
7970 ));
7971 }
7972
7973 // Evaluate the array argument
7974 let array = self.evaluate_internal(&args[0], data)?;
7975
7976 // Handle undefined input - return undefined
7977 if array.is_undefined() {
7978 return Ok(JValue::Undefined);
7979 }
7980
7981 // Handle null input
7982 if array.is_null() {
7983 return Ok(JValue::Undefined);
7984 }
7985
7986 // Coerce non-array values to single-element arrays
7987 // Track if input was a single value to unwrap result appropriately
7988 // Use references to avoid upfront cloning of all elements
7989 let single_holder;
7990 let (items, was_single_value): (&[JValue], bool) = match &array {
7991 JValue::Array(arr) => (arr.as_slice(), false),
7992 _ => {
7993 single_holder = [array];
7994 (&single_holder[..], true)
7995 }
7996 };
7997
7998 // Detect how many parameters the callback expects
7999 let param_count = self.get_callback_param_count(&args[1]);
8000
8001 // CompiledExpr fast path: direct lambda with 1 param, compilable body
8002 if param_count == 1 {
8003 if let AstNode::Lambda {
8004 params,
8005 body,
8006 signature: None,
8007 thunk: false,
8008 } = &args[1]
8009 {
8010 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8011 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8012 {
8013 let param_name = params[0].as_str();
8014 let mut result = Vec::with_capacity(items.len() / 2);
8015 let mut vars = HashMap::new();
8016 for item in items.iter() {
8017 vars.insert(param_name, item);
8018 let pred_result = eval_compiled(&compiled, data, Some(&vars))?;
8019 if compiled_is_truthy(&pred_result) {
8020 result.push(item.clone());
8021 }
8022 }
8023 if was_single_value {
8024 if result.len() == 1 {
8025 return Ok(result.remove(0));
8026 } else if result.is_empty() {
8027 return Ok(JValue::Undefined);
8028 }
8029 }
8030 return Ok(JValue::array(result));
8031 }
8032 }
8033 // Stored lambda variable fast path: $var with pre-compiled body
8034 if let AstNode::Variable(var_name) = &args[1] {
8035 if let Some(stored) = self.context.lookup_lambda(var_name) {
8036 if let Some(ref ce) = stored.compiled_body.clone() {
8037 let param_name = stored.params[0].clone();
8038 let captured_data = stored.captured_data.clone();
8039 let captured_env_clone = stored.captured_env.clone();
8040 let ce_clone = ce.clone();
8041 if !captured_env_clone.values().any(|v| {
8042 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8043 }) {
8044 let call_data = captured_data.as_ref().unwrap_or(data);
8045 let mut result = Vec::with_capacity(items.len() / 2);
8046 let mut vars: HashMap<&str, &JValue> = captured_env_clone
8047 .iter()
8048 .map(|(k, v)| (k.as_str(), v))
8049 .collect();
8050 for item in items.iter() {
8051 vars.insert(param_name.as_str(), item);
8052 let pred_result =
8053 eval_compiled(&ce_clone, call_data, Some(&vars))?;
8054 if compiled_is_truthy(&pred_result) {
8055 result.push(item.clone());
8056 }
8057 }
8058 if was_single_value {
8059 if result.len() == 1 {
8060 return Ok(result.remove(0));
8061 } else if result.is_empty() {
8062 return Ok(JValue::Undefined);
8063 }
8064 }
8065 return Ok(JValue::array(result));
8066 }
8067 }
8068 }
8069 }
8070 }
8071
8072 // Only create the array value if callback uses 3 parameters
8073 let arr_value = if param_count >= 3 {
8074 Some(JValue::array(items.to_vec()))
8075 } else {
8076 None
8077 };
8078
8079 let mut result = Vec::with_capacity(items.len() / 2);
8080
8081 for (index, item) in items.iter().enumerate() {
8082 // Build argument list based on what callback expects
8083 let call_args = match param_count {
8084 1 => vec![item.clone()],
8085 2 => vec![item.clone(), JValue::Number(index as f64)],
8086 _ => vec![
8087 item.clone(),
8088 JValue::Number(index as f64),
8089 arr_value.as_ref().unwrap().clone(),
8090 ],
8091 };
8092
8093 let predicate_result = self.apply_function(&args[1], &call_args, data)?;
8094 if self.is_truthy(&predicate_result) {
8095 result.push(item.clone());
8096 }
8097 }
8098
8099 // If input was a single value, return the single matching item
8100 // (or undefined if no match)
8101 if was_single_value {
8102 if result.len() == 1 {
8103 return Ok(result.remove(0));
8104 } else if result.is_empty() {
8105 return Ok(JValue::Undefined);
8106 }
8107 }
8108
8109 Ok(JValue::array(result))
8110 }
8111
8112 "reduce" => {
8113 if args.len() < 2 || args.len() > 3 {
8114 return Err(EvaluatorError::EvaluationError(
8115 "reduce() requires 2 or 3 arguments".to_string(),
8116 ));
8117 }
8118
8119 // Check that the callback function has at least 2 parameters
8120 if let AstNode::Lambda { params, .. } = &args[1] {
8121 if params.len() < 2 {
8122 return Err(EvaluatorError::EvaluationError(
8123 "D3050: The second argument of reduce must be a function with at least two arguments".to_string(),
8124 ));
8125 }
8126 } else if let AstNode::Function { name, .. } = &args[1] {
8127 // For now, we can't validate built-in function signatures here
8128 // But user-defined functions via lambda will be validated above
8129 let _ = name; // avoid unused warning
8130 }
8131
8132 // Evaluate the array argument
8133 let array = self.evaluate_internal(&args[0], data)?;
8134
8135 // Convert single value to array (JSONata reduce accepts single values)
8136 // Use references to avoid upfront cloning of all elements
8137 let single_holder;
8138 let items: &[JValue] = match &array {
8139 JValue::Array(arr) => arr.as_slice(),
8140 JValue::Null => return Ok(JValue::Null),
8141 _ => {
8142 single_holder = [array];
8143 &single_holder[..]
8144 }
8145 };
8146
8147 if items.is_empty() {
8148 // Return initial value if provided, otherwise null
8149 return if args.len() == 3 {
8150 self.evaluate_internal(&args[2], data)
8151 } else {
8152 Ok(JValue::Null)
8153 };
8154 }
8155
8156 // Get initial accumulator
8157 let mut accumulator = if args.len() == 3 {
8158 self.evaluate_internal(&args[2], data)?
8159 } else {
8160 items[0].clone()
8161 };
8162
8163 let start_idx = if args.len() == 3 { 0 } else { 1 };
8164
8165 // Detect how many parameters the callback expects
8166 let param_count = self.get_callback_param_count(&args[1]);
8167
8168 // CompiledExpr fast path: direct lambda with 2 params, compilable body
8169 if param_count == 2 {
8170 if let AstNode::Lambda {
8171 params,
8172 body,
8173 signature: None,
8174 thunk: false,
8175 } = &args[1]
8176 {
8177 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
8178 if let Some(compiled) = try_compile_expr_with_allowed_vars(body, &var_refs)
8179 {
8180 let acc_name = params[0].as_str();
8181 let item_name = params[1].as_str();
8182 for item in items[start_idx..].iter() {
8183 let vars: HashMap<&str, &JValue> =
8184 HashMap::from([(acc_name, &accumulator), (item_name, item)]);
8185 accumulator = eval_compiled(&compiled, data, Some(&vars))?;
8186 }
8187 return Ok(accumulator);
8188 }
8189 }
8190 // Stored lambda variable fast path: $var with pre-compiled body
8191 if let AstNode::Variable(var_name) = &args[1] {
8192 if let Some(stored) = self.context.lookup_lambda(var_name) {
8193 if stored.params.len() == 2 {
8194 if let Some(ref ce) = stored.compiled_body.clone() {
8195 let acc_param = stored.params[0].clone();
8196 let item_param = stored.params[1].clone();
8197 let captured_data = stored.captured_data.clone();
8198 let captured_env_clone = stored.captured_env.clone();
8199 let ce_clone = ce.clone();
8200 if !captured_env_clone.values().any(|v| {
8201 matches!(v, JValue::Lambda { .. } | JValue::Builtin { .. })
8202 }) {
8203 let call_data = captured_data.as_ref().unwrap_or(data);
8204 for item in items[start_idx..].iter() {
8205 let mut vars: HashMap<&str, &JValue> =
8206 captured_env_clone
8207 .iter()
8208 .map(|(k, v)| (k.as_str(), v))
8209 .collect();
8210 vars.insert(acc_param.as_str(), &accumulator);
8211 vars.insert(item_param.as_str(), item);
8212 // Evaluate and drop vars before assigning accumulator
8213 // to satisfy borrow checker (vars borrows accumulator)
8214 let new_acc =
8215 eval_compiled(&ce_clone, call_data, Some(&vars))?;
8216 drop(vars);
8217 accumulator = new_acc;
8218 }
8219 return Ok(accumulator);
8220 }
8221 }
8222 }
8223 }
8224 }
8225 }
8226
8227 // Only create the array value if callback uses 4 parameters
8228 let arr_value = if param_count >= 4 {
8229 Some(JValue::array(items.to_vec()))
8230 } else {
8231 None
8232 };
8233
8234 // Apply function to each element
8235 for (idx, item) in items[start_idx..].iter().enumerate() {
8236 // For reduce, the function receives (accumulator, value, index, array)
8237 // Callbacks may use any subset of these parameters
8238 let actual_idx = start_idx + idx;
8239
8240 // Build argument list based on what callback expects
8241 let call_args = match param_count {
8242 2 => vec![accumulator.clone(), item.clone()],
8243 3 => vec![
8244 accumulator.clone(),
8245 item.clone(),
8246 JValue::Number(actual_idx as f64),
8247 ],
8248 _ => vec![
8249 accumulator.clone(),
8250 item.clone(),
8251 JValue::Number(actual_idx as f64),
8252 arr_value.as_ref().unwrap().clone(),
8253 ],
8254 };
8255
8256 accumulator = self.apply_function(&args[1], &call_args, data)?;
8257 }
8258
8259 Ok(accumulator)
8260 }
8261
8262 "single" => {
8263 if args.is_empty() || args.len() > 2 {
8264 return Err(EvaluatorError::EvaluationError(
8265 "single() requires 1 or 2 arguments".to_string(),
8266 ));
8267 }
8268
8269 // Evaluate the array argument
8270 let array = self.evaluate_internal(&args[0], data)?;
8271
8272 // Convert to array (wrap single values)
8273 let arr = match array {
8274 JValue::Array(arr) => arr.to_vec(),
8275 JValue::Null => return Ok(JValue::Null),
8276 other => vec![other],
8277 };
8278
8279 if args.len() == 1 {
8280 // No predicate - array must have exactly 1 element
8281 match arr.len() {
8282 0 => Err(EvaluatorError::EvaluationError(
8283 "single() argument is empty".to_string(),
8284 )),
8285 1 => Ok(arr.into_iter().next().unwrap()),
8286 count => Err(EvaluatorError::EvaluationError(format!(
8287 "single() argument has {} values (expected exactly 1)",
8288 count
8289 ))),
8290 }
8291 } else {
8292 // With predicate - find exactly 1 matching element
8293 let arr_value = JValue::array(arr.clone());
8294 let mut matches = Vec::new();
8295 for (index, item) in arr.into_iter().enumerate() {
8296 // Apply predicate function with (item, index, array)
8297 let predicate_result = self.apply_function(
8298 &args[1],
8299 &[
8300 item.clone(),
8301 JValue::Number(index as f64),
8302 arr_value.clone(),
8303 ],
8304 data,
8305 )?;
8306 if self.is_truthy(&predicate_result) {
8307 matches.push(item);
8308 }
8309 }
8310
8311 match matches.len() {
8312 0 => Err(EvaluatorError::EvaluationError(
8313 "single() predicate matches no values".to_string(),
8314 )),
8315 1 => Ok(matches.into_iter().next().unwrap()),
8316 count => Err(EvaluatorError::EvaluationError(format!(
8317 "single() predicate matches {} values (expected exactly 1)",
8318 count
8319 ))),
8320 }
8321 }
8322 }
8323
8324 "each" => {
8325 // $each(object, function) - iterate over object, applying function to each value/key pair
8326 // Returns an array of the function results
8327 if args.is_empty() || args.len() > 2 {
8328 return Err(EvaluatorError::EvaluationError(
8329 "each() requires 1 or 2 arguments".to_string(),
8330 ));
8331 }
8332
8333 // Determine which argument is the object and which is the function
8334 let (obj_value, func_arg) = if args.len() == 1 {
8335 // Single argument: use current data as object
8336 (data.clone(), &args[0])
8337 } else {
8338 // Two arguments: first is object, second is function
8339 (self.evaluate_internal(&args[0], data)?, &args[1])
8340 };
8341
8342 // Detect how many parameters the callback expects
8343 let param_count = self.get_callback_param_count(func_arg);
8344
8345 match obj_value {
8346 JValue::Object(obj) => {
8347 let mut result = Vec::new();
8348 for (key, value) in obj.iter() {
8349 // Build argument list based on what callback expects
8350 // The callback receives the value as the first argument and key as second
8351 let call_args = match param_count {
8352 1 => vec![value.clone()],
8353 _ => vec![value.clone(), JValue::string(key.clone())],
8354 };
8355
8356 let fn_result = self.apply_function(func_arg, &call_args, data)?;
8357 // Skip undefined results (similar to map behavior)
8358 if !fn_result.is_null() && !fn_result.is_undefined() {
8359 result.push(fn_result);
8360 }
8361 }
8362 Ok(JValue::array(result))
8363 }
8364 JValue::Null => Ok(JValue::Null),
8365 _ => Err(EvaluatorError::TypeError(
8366 "each() first argument must be an object".to_string(),
8367 )),
8368 }
8369 }
8370
8371 "not" => {
8372 if evaluated_args.len() != 1 {
8373 return Err(EvaluatorError::EvaluationError(
8374 "not() requires exactly 1 argument".to_string(),
8375 ));
8376 }
8377 // $not(x) returns the logical negation of x
8378 // null is falsy, so $not(null) = true; undefined stays undefined
8379 if evaluated_args[0].is_undefined() {
8380 return Ok(JValue::Undefined);
8381 }
8382 Ok(JValue::Bool(!self.is_truthy(&evaluated_args[0])))
8383 }
8384 "boolean" => {
8385 if evaluated_args.len() != 1 {
8386 return Err(EvaluatorError::EvaluationError(
8387 "boolean() requires exactly 1 argument".to_string(),
8388 ));
8389 }
8390 if evaluated_args[0].is_undefined() {
8391 return Ok(JValue::Undefined);
8392 }
8393 Ok(functions::boolean::boolean(&evaluated_args[0])?)
8394 }
8395 "type" => {
8396 if evaluated_args.len() != 1 {
8397 return Err(EvaluatorError::EvaluationError(
8398 "type() requires exactly 1 argument".to_string(),
8399 ));
8400 }
8401 // Return type string
8402 // In JavaScript: $type(undefined) returns undefined, $type(null) returns "null"
8403 // We use a special marker object to distinguish undefined from null
8404 match &evaluated_args[0] {
8405 JValue::Null => Ok(JValue::string("null")),
8406 JValue::Bool(_) => Ok(JValue::string("boolean")),
8407 JValue::Number(_) => Ok(JValue::string("number")),
8408 JValue::String(_) => Ok(JValue::string("string")),
8409 JValue::Array(_) => Ok(JValue::string("array")),
8410 JValue::Object(_) => Ok(JValue::string("object")),
8411 JValue::Undefined => Ok(JValue::Undefined),
8412 JValue::Lambda { .. } | JValue::Builtin { .. } => {
8413 Ok(JValue::string("function"))
8414 }
8415 JValue::Regex { .. } => Ok(JValue::string("regex")),
8416 }
8417 }
8418
8419 "base64encode" => {
8420 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8421 return Ok(JValue::Null);
8422 }
8423 if evaluated_args.len() != 1 {
8424 return Err(EvaluatorError::EvaluationError(
8425 "base64encode() requires exactly 1 argument".to_string(),
8426 ));
8427 }
8428 match &evaluated_args[0] {
8429 JValue::String(s) => Ok(functions::encoding::base64encode(s)?),
8430 _ => Err(EvaluatorError::TypeError(
8431 "base64encode() requires a string argument".to_string(),
8432 )),
8433 }
8434 }
8435 "base64decode" => {
8436 if evaluated_args.is_empty() || evaluated_args[0].is_null() {
8437 return Ok(JValue::Null);
8438 }
8439 if evaluated_args.len() != 1 {
8440 return Err(EvaluatorError::EvaluationError(
8441 "base64decode() requires exactly 1 argument".to_string(),
8442 ));
8443 }
8444 match &evaluated_args[0] {
8445 JValue::String(s) => Ok(functions::encoding::base64decode(s)?),
8446 _ => Err(EvaluatorError::TypeError(
8447 "base64decode() requires a string argument".to_string(),
8448 )),
8449 }
8450 }
8451 "encodeUrlComponent" => {
8452 if evaluated_args.len() != 1 {
8453 return Err(EvaluatorError::EvaluationError(
8454 "encodeUrlComponent() requires exactly 1 argument".to_string(),
8455 ));
8456 }
8457 if evaluated_args[0].is_null() {
8458 return Ok(JValue::Null);
8459 }
8460 if evaluated_args[0].is_undefined() {
8461 return Ok(JValue::Undefined);
8462 }
8463 match &evaluated_args[0] {
8464 JValue::String(s) => Ok(functions::encoding::encode_url_component(s)?),
8465 _ => Err(EvaluatorError::TypeError(
8466 "encodeUrlComponent() requires a string argument".to_string(),
8467 )),
8468 }
8469 }
8470 "decodeUrlComponent" => {
8471 if evaluated_args.len() != 1 {
8472 return Err(EvaluatorError::EvaluationError(
8473 "decodeUrlComponent() requires exactly 1 argument".to_string(),
8474 ));
8475 }
8476 if evaluated_args[0].is_null() {
8477 return Ok(JValue::Null);
8478 }
8479 if evaluated_args[0].is_undefined() {
8480 return Ok(JValue::Undefined);
8481 }
8482 match &evaluated_args[0] {
8483 JValue::String(s) => Ok(functions::encoding::decode_url_component(s)?),
8484 _ => Err(EvaluatorError::TypeError(
8485 "decodeUrlComponent() requires a string argument".to_string(),
8486 )),
8487 }
8488 }
8489 "encodeUrl" => {
8490 if evaluated_args.len() != 1 {
8491 return Err(EvaluatorError::EvaluationError(
8492 "encodeUrl() requires exactly 1 argument".to_string(),
8493 ));
8494 }
8495 if evaluated_args[0].is_null() {
8496 return Ok(JValue::Null);
8497 }
8498 if evaluated_args[0].is_undefined() {
8499 return Ok(JValue::Undefined);
8500 }
8501 match &evaluated_args[0] {
8502 JValue::String(s) => Ok(functions::encoding::encode_url(s)?),
8503 _ => Err(EvaluatorError::TypeError(
8504 "encodeUrl() requires a string argument".to_string(),
8505 )),
8506 }
8507 }
8508 "decodeUrl" => {
8509 if evaluated_args.len() != 1 {
8510 return Err(EvaluatorError::EvaluationError(
8511 "decodeUrl() requires exactly 1 argument".to_string(),
8512 ));
8513 }
8514 if evaluated_args[0].is_null() {
8515 return Ok(JValue::Null);
8516 }
8517 if evaluated_args[0].is_undefined() {
8518 return Ok(JValue::Undefined);
8519 }
8520 match &evaluated_args[0] {
8521 JValue::String(s) => Ok(functions::encoding::decode_url(s)?),
8522 _ => Err(EvaluatorError::TypeError(
8523 "decodeUrl() requires a string argument".to_string(),
8524 )),
8525 }
8526 }
8527
8528 "error" => {
8529 // $error(message) - throw error with custom message
8530 if evaluated_args.is_empty() {
8531 // No message provided
8532 return Err(EvaluatorError::EvaluationError(
8533 "D3137: $error() function evaluated".to_string(),
8534 ));
8535 }
8536
8537 match &evaluated_args[0] {
8538 JValue::String(s) => {
8539 Err(EvaluatorError::EvaluationError(format!("D3137: {}", s)))
8540 }
8541 _ => Err(EvaluatorError::TypeError(
8542 "T0410: Argument 1 of function error does not match function signature"
8543 .to_string(),
8544 )),
8545 }
8546 }
8547 "assert" => {
8548 // $assert(condition, message) - throw error if condition is false
8549 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8550 return Err(EvaluatorError::EvaluationError(
8551 "assert() requires 1 or 2 arguments".to_string(),
8552 ));
8553 }
8554
8555 // First argument must be a boolean
8556 let condition = match &evaluated_args[0] {
8557 JValue::Bool(b) => *b,
8558 _ => {
8559 return Err(EvaluatorError::TypeError(
8560 "T0410: Argument 1 of function $assert does not match function signature".to_string(),
8561 ));
8562 }
8563 };
8564
8565 if !condition {
8566 let message = if evaluated_args.len() == 2 {
8567 match &evaluated_args[1] {
8568 JValue::String(s) => s.clone(),
8569 _ => Rc::from("$assert() statement failed"),
8570 }
8571 } else {
8572 Rc::from("$assert() statement failed")
8573 };
8574 return Err(EvaluatorError::EvaluationError(format!(
8575 "D3141: {}",
8576 message
8577 )));
8578 }
8579
8580 Ok(JValue::Null)
8581 }
8582
8583 "eval" => {
8584 // $eval(expression [, context]) - parse and evaluate a JSONata expression at runtime
8585 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8586 return Err(EvaluatorError::EvaluationError(
8587 "T0410: Argument 1 of function $eval must be a string".to_string(),
8588 ));
8589 }
8590
8591 // If the first argument is null/undefined, return undefined
8592 if evaluated_args[0].is_null() {
8593 return Ok(JValue::Null);
8594 }
8595 if evaluated_args[0].is_undefined() {
8596 return Ok(JValue::Undefined);
8597 }
8598
8599 // First argument must be a string expression
8600 let expr_str = match &evaluated_args[0] {
8601 JValue::String(s) => &**s,
8602 _ => {
8603 return Err(EvaluatorError::EvaluationError(
8604 "T0410: Argument 1 of function $eval must be a string".to_string(),
8605 ));
8606 }
8607 };
8608
8609 // Parse the expression
8610 let parsed_ast = match parser::parse(expr_str) {
8611 Ok(ast) => ast,
8612 Err(e) => {
8613 // D3120 is the error code for parse errors in $eval
8614 return Err(EvaluatorError::EvaluationError(format!(
8615 "D3120: The expression passed to $eval cannot be parsed: {}",
8616 e
8617 )));
8618 }
8619 };
8620
8621 // Determine the context to use for evaluation
8622 let eval_context = if evaluated_args.len() == 2 {
8623 &evaluated_args[1]
8624 } else {
8625 data
8626 };
8627
8628 // Evaluate the parsed expression
8629 match self.evaluate_internal(&parsed_ast, eval_context) {
8630 Ok(result) => Ok(result),
8631 Err(e) => {
8632 // D3121 is the error code for evaluation errors in $eval
8633 let err_msg = e.to_string();
8634 if err_msg.starts_with("D3121") || err_msg.contains("Unknown function") {
8635 Err(EvaluatorError::EvaluationError(format!(
8636 "D3121: {}",
8637 err_msg
8638 )))
8639 } else {
8640 Err(e)
8641 }
8642 }
8643 }
8644 }
8645
8646 "now" => {
8647 if !evaluated_args.is_empty() {
8648 return Err(EvaluatorError::EvaluationError(
8649 "now() takes no arguments".to_string(),
8650 ));
8651 }
8652 Ok(crate::datetime::now())
8653 }
8654
8655 "millis" => {
8656 if !evaluated_args.is_empty() {
8657 return Err(EvaluatorError::EvaluationError(
8658 "millis() takes no arguments".to_string(),
8659 ));
8660 }
8661 Ok(crate::datetime::millis())
8662 }
8663
8664 "toMillis" => {
8665 if evaluated_args.is_empty() || evaluated_args.len() > 2 {
8666 return Err(EvaluatorError::EvaluationError(
8667 "toMillis() requires 1 or 2 arguments".to_string(),
8668 ));
8669 }
8670
8671 match &evaluated_args[0] {
8672 JValue::String(s) => {
8673 // Optional second argument is a picture string for custom parsing
8674 if evaluated_args.len() == 2 {
8675 match &evaluated_args[1] {
8676 JValue::String(picture) => {
8677 // Use custom picture format parsing
8678 Ok(crate::datetime::to_millis_with_picture(s, picture)?)
8679 }
8680 JValue::Null => Ok(JValue::Null),
8681 JValue::Undefined => Ok(JValue::Undefined),
8682 _ => Err(EvaluatorError::TypeError(
8683 "toMillis() second argument must be a string".to_string(),
8684 )),
8685 }
8686 } else {
8687 // Use ISO 8601 partial date parsing
8688 Ok(crate::datetime::to_millis(s)?)
8689 }
8690 }
8691 JValue::Null => Ok(JValue::Null),
8692 JValue::Undefined => Ok(JValue::Undefined),
8693 _ => Err(EvaluatorError::TypeError(
8694 "toMillis() requires a string argument".to_string(),
8695 )),
8696 }
8697 }
8698
8699 "fromMillis" => {
8700 if evaluated_args.is_empty() || evaluated_args.len() > 3 {
8701 return Err(EvaluatorError::EvaluationError(
8702 "fromMillis() requires 1 to 3 arguments".to_string(),
8703 ));
8704 }
8705
8706 match &evaluated_args[0] {
8707 JValue::Number(n) => {
8708 let millis = (if n.fract() == 0.0 {
8709 Ok(*n as i64)
8710 } else {
8711 Err(())
8712 })
8713 .map_err(|_| {
8714 EvaluatorError::TypeError(
8715 "fromMillis() requires an integer".to_string(),
8716 )
8717 })?;
8718
8719 let picture = match evaluated_args.get(1) {
8720 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
8721 Some(JValue::String(s)) => Some(s.to_string()),
8722 Some(_) => {
8723 return Err(EvaluatorError::TypeError(
8724 "fromMillis() second argument must be a string".to_string(),
8725 ))
8726 }
8727 };
8728 let timezone = match evaluated_args.get(2) {
8729 None | Some(JValue::Undefined) | Some(JValue::Null) => None,
8730 Some(JValue::String(s)) => Some(s.to_string()),
8731 Some(_) => {
8732 return Err(EvaluatorError::TypeError(
8733 "fromMillis() third argument must be a string".to_string(),
8734 ))
8735 }
8736 };
8737
8738 Ok(crate::datetime::from_millis_with_picture(
8739 millis,
8740 picture.as_deref(),
8741 timezone.as_deref(),
8742 )?)
8743 }
8744 JValue::Null => Ok(JValue::Null),
8745 JValue::Undefined => Ok(JValue::Undefined),
8746 _ => Err(EvaluatorError::TypeError(
8747 "fromMillis() requires a number argument".to_string(),
8748 )),
8749 }
8750 }
8751
8752 _ => Err(EvaluatorError::ReferenceError(format!(
8753 "Unknown function: {}",
8754 name
8755 ))),
8756 }
8757 }
8758
8759 /// Apply a function (lambda or expression) to values
8760 ///
8761 /// This handles both:
8762 /// 1. Lambda nodes: function($x) { $x * 2 } - binds parameters and evaluates body
8763 /// 2. Simple expressions: price * 2 - evaluates with values as context
8764 fn apply_function(
8765 &mut self,
8766 func_node: &AstNode,
8767 values: &[JValue],
8768 data: &JValue,
8769 ) -> Result<JValue, EvaluatorError> {
8770 match func_node {
8771 AstNode::Lambda {
8772 params,
8773 body,
8774 signature,
8775 thunk,
8776 } => {
8777 // Direct lambda - invoke it
8778 self.invoke_lambda(params, body, signature.as_ref(), values, data, *thunk)
8779 }
8780 AstNode::Function {
8781 name,
8782 args,
8783 is_builtin,
8784 } => {
8785 // Function call - check if it has placeholders (partial application)
8786 let has_placeholder = args.iter().any(|arg| matches!(arg, AstNode::Placeholder));
8787
8788 if has_placeholder {
8789 // This is a partial application - evaluate it to get the lambda value
8790 let partial_lambda =
8791 self.create_partial_application(name, args, *is_builtin, data)?;
8792
8793 // Now invoke the partial lambda with the provided values
8794 if let Some(stored) = self.lookup_lambda_from_value(&partial_lambda) {
8795 return self.invoke_stored_lambda(&stored, values, data);
8796 }
8797 Err(EvaluatorError::EvaluationError(
8798 "Failed to apply partial application".to_string(),
8799 ))
8800 } else {
8801 // Regular function call without placeholders
8802 // Evaluate it and apply if it returns a function
8803 let result = self.evaluate_internal(func_node, data)?;
8804
8805 // Check if result is a lambda value
8806 if let Some(stored) = self.lookup_lambda_from_value(&result) {
8807 return self.invoke_stored_lambda(&stored, values, data);
8808 }
8809
8810 // Otherwise just return the result
8811 Ok(result)
8812 }
8813 }
8814 AstNode::Variable(var_name) => {
8815 // Check if this variable holds a stored lambda
8816 if let Some(stored_lambda) = self.context.lookup_lambda(var_name).cloned() {
8817 self.invoke_stored_lambda(&stored_lambda, values, data)
8818 } else if let Some(value) = self.context.lookup(var_name).cloned() {
8819 // Check if this variable holds a lambda value
8820 // This handles lambdas passed as bound arguments in partial applications
8821 if let Some(stored) = self.lookup_lambda_from_value(&value) {
8822 return self.invoke_stored_lambda(&stored, values, data);
8823 }
8824 // Regular variable value - evaluate with first value as context
8825 if values.is_empty() {
8826 self.evaluate_internal(func_node, data)
8827 } else {
8828 self.evaluate_internal(func_node, &values[0])
8829 }
8830 } else if self.is_builtin_function(var_name) {
8831 // This is a built-in function reference (e.g., $string, $number)
8832 // Call it directly with the provided values (already evaluated)
8833 self.call_builtin_with_values(var_name, values)
8834 } else {
8835 // Unknown variable - evaluate with first value as context
8836 if values.is_empty() {
8837 self.evaluate_internal(func_node, data)
8838 } else {
8839 self.evaluate_internal(func_node, &values[0])
8840 }
8841 }
8842 }
8843 _ => {
8844 // For non-lambda expressions, evaluate with first value as context
8845 if values.is_empty() {
8846 self.evaluate_internal(func_node, data)
8847 } else {
8848 self.evaluate_internal(func_node, &values[0])
8849 }
8850 }
8851 }
8852 }
8853
8854 /// Execute a transform operator on the bound $ value
8855 fn execute_transform(
8856 &mut self,
8857 location: &AstNode,
8858 update: &AstNode,
8859 delete: Option<&AstNode>,
8860 _original_data: &JValue,
8861 ) -> Result<JValue, EvaluatorError> {
8862 // Get the input value from $ binding
8863 let input = self
8864 .context
8865 .lookup("$")
8866 .ok_or_else(|| {
8867 EvaluatorError::EvaluationError("Transform requires $ binding".to_string())
8868 })?
8869 .clone();
8870
8871 // Evaluate location expression on the input to get objects to transform
8872 let located_objects = self.evaluate_internal(location, &input)?;
8873
8874 // Collect target objects into a vector for comparison
8875 let targets: Vec<JValue> = match located_objects {
8876 JValue::Array(arr) => arr.to_vec(),
8877 JValue::Object(_) => vec![located_objects],
8878 JValue::Null => Vec::new(),
8879 other => vec![other],
8880 };
8881
8882 // Validate update parameter - must be an object constructor
8883 // We need to check this before evaluation in case of errors
8884 // For now, we'll validate after evaluation in the transform helper
8885
8886 // Parse delete field names if provided
8887 let delete_fields: Vec<String> = if let Some(delete_node) = delete {
8888 let delete_val = self.evaluate_internal(delete_node, &input)?;
8889 match delete_val {
8890 JValue::Array(arr) => arr
8891 .iter()
8892 .filter_map(|v| match v {
8893 JValue::String(s) => Some(s.to_string()),
8894 _ => None,
8895 })
8896 .collect(),
8897 JValue::String(s) => vec![s.to_string()],
8898 JValue::Null | JValue::Undefined => Vec::new(), // Undefined variable is treated as no deletion
8899 _ => {
8900 // Delete parameter must be an array of strings or a string
8901 return Err(EvaluatorError::EvaluationError(
8902 "T2012: The third argument of the transform operator must be an array of strings".to_string()
8903 ));
8904 }
8905 }
8906 } else {
8907 Vec::new()
8908 };
8909
8910 // Recursive helper to apply transformation throughout the structure
8911 fn apply_transform_deep(
8912 evaluator: &mut Evaluator,
8913 value: &JValue,
8914 targets: &[JValue],
8915 update: &AstNode,
8916 delete_fields: &[String],
8917 ) -> Result<JValue, EvaluatorError> {
8918 // Check if this value is one of the targets to transform
8919 // Use JValue's PartialEq for semantic equality comparison
8920 if targets.iter().any(|t| t == value) {
8921 // Transform this object
8922 if let JValue::Object(map_rc) = value.clone() {
8923 let mut map = (*map_rc).clone();
8924 let update_val = evaluator.evaluate_internal(update, value)?;
8925 // Validate that update evaluates to an object or null (undefined)
8926 match update_val {
8927 JValue::Object(update_map) => {
8928 for (key, val) in update_map.iter() {
8929 map.insert(key.clone(), val.clone());
8930 }
8931 }
8932 JValue::Null | JValue::Undefined => {
8933 // Null/undefined means no updates, just continue to deletions
8934 }
8935 _ => {
8936 return Err(EvaluatorError::EvaluationError(
8937 "T2011: The second argument of the transform operator must evaluate to an object".to_string()
8938 ));
8939 }
8940 }
8941 for field in delete_fields {
8942 map.shift_remove(field);
8943 }
8944 return Ok(JValue::object(map));
8945 }
8946 return Ok(value.clone());
8947 }
8948
8949 // Otherwise, recursively process children to find and transform targets
8950 match value {
8951 JValue::Object(map) => {
8952 let mut new_map = IndexMap::new();
8953 for (k, v) in map.iter() {
8954 new_map.insert(
8955 k.clone(),
8956 apply_transform_deep(evaluator, v, targets, update, delete_fields)?,
8957 );
8958 }
8959 Ok(JValue::object(new_map))
8960 }
8961 JValue::Array(arr) => {
8962 let mut new_arr = Vec::new();
8963 for item in arr.iter() {
8964 new_arr.push(apply_transform_deep(
8965 evaluator,
8966 item,
8967 targets,
8968 update,
8969 delete_fields,
8970 )?);
8971 }
8972 Ok(JValue::array(new_arr))
8973 }
8974 _ => Ok(value.clone()),
8975 }
8976 }
8977
8978 // Apply transformation recursively starting from input
8979 apply_transform_deep(self, &input, &targets, update, &delete_fields)
8980 }
8981
8982 /// Helper to invoke a lambda with given parameters
8983 fn invoke_lambda(
8984 &mut self,
8985 params: &[String],
8986 body: &AstNode,
8987 signature: Option<&String>,
8988 values: &[JValue],
8989 data: &JValue,
8990 thunk: bool,
8991 ) -> Result<JValue, EvaluatorError> {
8992 self.invoke_lambda_with_env(params, body, signature, values, data, None, None, thunk)
8993 }
8994
8995 /// Invoke a lambda with optional captured environment (for closures)
8996 fn invoke_lambda_with_env(
8997 &mut self,
8998 params: &[String],
8999 body: &AstNode,
9000 signature: Option<&String>,
9001 values: &[JValue],
9002 data: &JValue,
9003 captured_env: Option<&HashMap<String, JValue>>,
9004 captured_data: Option<&JValue>,
9005 thunk: bool,
9006 ) -> Result<JValue, EvaluatorError> {
9007 // If this is a thunk (has tail calls), use TCO trampoline
9008 if thunk {
9009 let stored = StoredLambda {
9010 params: params.to_vec(),
9011 body: body.clone(),
9012 compiled_body: None, // Thunks use TCO, not the compiled fast path
9013 signature: signature.cloned(),
9014 captured_env: captured_env.cloned().unwrap_or_default(),
9015 captured_data: captured_data.cloned(),
9016 thunk,
9017 };
9018 return self.invoke_lambda_with_tco(&stored, values, data);
9019 }
9020
9021 // Validate signature if present, and get coerced arguments
9022 // Push a new scope for this lambda invocation
9023 self.context.push_scope();
9024
9025 // First apply captured environment (for closures)
9026 if let Some(env) = captured_env {
9027 for (name, value) in env {
9028 self.context.bind(name.clone(), value.clone());
9029 }
9030 }
9031
9032 if let Some(sig_str) = signature {
9033 // Validate and coerce arguments with signature
9034 let coerced_values = match crate::signature::Signature::parse(sig_str) {
9035 Ok(sig) => match sig.validate_and_coerce(values, data) {
9036 Ok(coerced) => coerced,
9037 Err(e) => {
9038 self.context.pop_scope();
9039 match e {
9040 crate::signature::SignatureError::UndefinedArgument => {
9041 return Ok(JValue::Null);
9042 }
9043 crate::signature::SignatureError::ArgumentTypeMismatch {
9044 index,
9045 expected,
9046 } => {
9047 return Err(EvaluatorError::TypeError(
9048 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9049 ));
9050 }
9051 crate::signature::SignatureError::ArrayTypeMismatch {
9052 index,
9053 expected,
9054 } => {
9055 return Err(EvaluatorError::TypeError(format!(
9056 "T0412: Argument {} of function must be an array of {}",
9057 index, expected
9058 )));
9059 }
9060 crate::signature::SignatureError::ContextTypeMismatch {
9061 index,
9062 expected,
9063 } => {
9064 return Err(EvaluatorError::TypeError(format!(
9065 "T0411: Context value at argument {} does not match function signature (expected {})",
9066 index, expected
9067 )));
9068 }
9069 _ => {
9070 return Err(EvaluatorError::TypeError(format!(
9071 "Signature validation failed: {}",
9072 e
9073 )));
9074 }
9075 }
9076 }
9077 },
9078 Err(e) => {
9079 self.context.pop_scope();
9080 return Err(EvaluatorError::EvaluationError(format!(
9081 "Invalid signature: {}",
9082 e
9083 )));
9084 }
9085 };
9086 // Bind coerced values to params
9087 for (i, param) in params.iter().enumerate() {
9088 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Undefined);
9089 self.context.bind(param.clone(), value);
9090 }
9091 } else {
9092 // No signature - bind directly from values slice (no allocation)
9093 for (i, param) in params.iter().enumerate() {
9094 let value = values.get(i).cloned().unwrap_or(JValue::Undefined);
9095 self.context.bind(param.clone(), value);
9096 }
9097 }
9098
9099 // Check if this is a partial application (body is a special marker string)
9100 if let AstNode::String(body_str) = body {
9101 if body_str.starts_with("__partial_call:") {
9102 // Parse the partial call info
9103 let parts: Vec<&str> = body_str.split(':').collect();
9104 if parts.len() >= 4 {
9105 let func_name = parts[1];
9106 let is_builtin = parts[2] == "true";
9107 let total_args: usize = parts[3].parse().unwrap_or(0);
9108
9109 // Get placeholder positions from captured env
9110 let placeholder_positions: Vec<usize> = if let Some(env) = captured_env {
9111 if let Some(JValue::Array(positions)) = env.get("__placeholder_positions") {
9112 positions
9113 .iter()
9114 .filter_map(|v| v.as_f64().map(|n| n as usize))
9115 .collect()
9116 } else {
9117 vec![]
9118 }
9119 } else {
9120 vec![]
9121 };
9122
9123 // Reconstruct the full argument list
9124 let mut full_args: Vec<JValue> = vec![JValue::Null; total_args];
9125
9126 // Fill in bound arguments from captured environment
9127 if let Some(env) = captured_env {
9128 for (key, value) in env {
9129 if key.starts_with("__bound_arg_") {
9130 if let Ok(pos) = key[12..].parse::<usize>() {
9131 if pos < total_args {
9132 full_args[pos] = value.clone();
9133 }
9134 }
9135 }
9136 }
9137 }
9138
9139 // Fill in placeholder positions with provided values
9140 for (i, &pos) in placeholder_positions.iter().enumerate() {
9141 if pos < total_args {
9142 let value = values.get(i).cloned().unwrap_or(JValue::Null);
9143 full_args[pos] = value;
9144 }
9145 }
9146
9147 // Pop lambda scope, then push a new scope for temp args
9148 self.context.pop_scope();
9149 self.context.push_scope();
9150
9151 // Build AST nodes for the function call arguments
9152 let mut temp_args: Vec<AstNode> = Vec::new();
9153 for (i, value) in full_args.iter().enumerate() {
9154 let temp_name = format!("__temp_arg_{}", i);
9155 self.context.bind(temp_name.clone(), value.clone());
9156 temp_args.push(AstNode::Variable(temp_name));
9157 }
9158
9159 // Call the original function
9160 let result =
9161 self.evaluate_function_call(func_name, &temp_args, is_builtin, data);
9162
9163 // Pop temp scope
9164 self.context.pop_scope();
9165
9166 return result;
9167 }
9168 }
9169 }
9170
9171 // Evaluate lambda body (normal case)
9172 // Use captured_data for lexical scoping if available, otherwise use call-site data
9173 let body_data = captured_data.unwrap_or(data);
9174 let result = self.evaluate_internal(body, body_data)?;
9175
9176 // Pop lambda scope, preserving any lambdas referenced by the return value
9177 // Fast path: scalar results can never contain lambda references
9178 let is_scalar = matches!(
9179 &result,
9180 JValue::Number(_)
9181 | JValue::Bool(_)
9182 | JValue::String(_)
9183 | JValue::Null
9184 | JValue::Undefined
9185 );
9186 if is_scalar {
9187 self.context.pop_scope();
9188 } else {
9189 let lambdas_to_keep = self.extract_lambda_ids(&result);
9190 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9191 }
9192
9193 Ok(result)
9194 }
9195
9196 /// Invoke a lambda with tail call optimization using a trampoline
9197 /// This method uses an iterative loop to handle tail-recursive calls without
9198 /// growing the stack, enabling deep recursion for tail-recursive functions.
9199 fn invoke_lambda_with_tco(
9200 &mut self,
9201 stored_lambda: &StoredLambda,
9202 initial_args: &[JValue],
9203 data: &JValue,
9204 ) -> Result<JValue, EvaluatorError> {
9205 let mut current_lambda = stored_lambda.clone();
9206 let mut current_args = initial_args.to_vec();
9207 let mut current_data = data.clone();
9208
9209 // Maximum number of tail call iterations to prevent infinite loops
9210 // This is much higher than non-TCO depth limit since TCO doesn't grow the stack
9211 const MAX_TCO_ITERATIONS: usize = 100_000;
9212 let mut iterations = 0;
9213
9214 // Push a persistent scope for the TCO trampoline loop.
9215 // This scope persists across all iterations so that lambdas defined
9216 // in one iteration (like recursive $iter) remain available in subsequent ones.
9217 self.context.push_scope();
9218
9219 // Trampoline loop - keeps evaluating until we get a final value
9220 let result = loop {
9221 iterations += 1;
9222 if iterations > MAX_TCO_ITERATIONS {
9223 self.context.pop_scope();
9224 return Err(EvaluatorError::EvaluationError(
9225 "U1001: Stack overflow - maximum recursion depth (500) exceeded".to_string(),
9226 ));
9227 }
9228
9229 // Evaluate the lambda body within the persistent scope
9230 let result =
9231 self.invoke_lambda_body_for_tco(¤t_lambda, ¤t_args, ¤t_data)?;
9232
9233 match result {
9234 LambdaResult::JValue(v) => break v,
9235 LambdaResult::TailCall { lambda, args, data } => {
9236 // Continue with the tail call - no stack growth
9237 current_lambda = *lambda;
9238 current_args = args;
9239 current_data = data;
9240 }
9241 }
9242 };
9243
9244 // Pop the persistent TCO scope, preserving lambdas referenced by the result
9245 let lambdas_to_keep = self.extract_lambda_ids(&result);
9246 self.context.pop_scope_preserving_lambdas(&lambdas_to_keep);
9247
9248 Ok(result)
9249 }
9250
9251 /// Evaluate a lambda body, detecting tail calls for TCO
9252 /// Returns either a final value or a tail call continuation.
9253 /// NOTE: Does not push/pop its own scope - the caller (invoke_lambda_with_tco)
9254 /// manages the persistent scope for the trampoline loop.
9255 fn invoke_lambda_body_for_tco(
9256 &mut self,
9257 lambda: &StoredLambda,
9258 values: &[JValue],
9259 data: &JValue,
9260 ) -> Result<LambdaResult, EvaluatorError> {
9261 // Validate signature if present
9262 let coerced_values = if let Some(sig_str) = &lambda.signature {
9263 match crate::signature::Signature::parse(sig_str) {
9264 Ok(sig) => match sig.validate_and_coerce(values, data) {
9265 Ok(coerced) => coerced,
9266 Err(e) => match e {
9267 crate::signature::SignatureError::UndefinedArgument => {
9268 return Ok(LambdaResult::JValue(JValue::Null));
9269 }
9270 crate::signature::SignatureError::ArgumentTypeMismatch {
9271 index,
9272 expected,
9273 } => {
9274 return Err(EvaluatorError::TypeError(
9275 format!("T0410: Argument {} of function does not match function signature (expected {})", index, expected)
9276 ));
9277 }
9278 crate::signature::SignatureError::ArrayTypeMismatch { index, expected } => {
9279 return Err(EvaluatorError::TypeError(format!(
9280 "T0412: Argument {} of function must be an array of {}",
9281 index, expected
9282 )));
9283 }
9284 crate::signature::SignatureError::ContextTypeMismatch {
9285 index,
9286 expected,
9287 } => {
9288 return Err(EvaluatorError::TypeError(format!(
9289 "T0411: Context value at argument {} does not match function signature (expected {})",
9290 index, expected
9291 )));
9292 }
9293 _ => {
9294 return Err(EvaluatorError::TypeError(format!(
9295 "Signature validation failed: {}",
9296 e
9297 )));
9298 }
9299 },
9300 },
9301 Err(e) => {
9302 return Err(EvaluatorError::EvaluationError(format!(
9303 "Invalid signature: {}",
9304 e
9305 )));
9306 }
9307 }
9308 } else {
9309 values.to_vec()
9310 };
9311
9312 // Bind directly into the persistent scope (managed by invoke_lambda_with_tco)
9313 // Apply captured environment
9314 for (name, value) in &lambda.captured_env {
9315 self.context.bind(name.clone(), value.clone());
9316 }
9317
9318 // Bind parameters
9319 for (i, param) in lambda.params.iter().enumerate() {
9320 let value = coerced_values.get(i).cloned().unwrap_or(JValue::Null);
9321 self.context.bind(param.clone(), value);
9322 }
9323
9324 // Evaluate the body with tail call detection
9325 let body_data = lambda.captured_data.as_ref().unwrap_or(data);
9326 self.evaluate_for_tco(&lambda.body, body_data)
9327 }
9328
9329 /// Evaluate an expression for TCO, detecting tail calls
9330 /// Returns LambdaResult::TailCall if the expression is a function call to a user lambda
9331 fn evaluate_for_tco(
9332 &mut self,
9333 node: &AstNode,
9334 data: &JValue,
9335 ) -> Result<LambdaResult, EvaluatorError> {
9336 match node {
9337 // Conditional: evaluate condition, then evaluate the chosen branch for TCO
9338 AstNode::Conditional {
9339 condition,
9340 then_branch,
9341 else_branch,
9342 } => {
9343 let cond_value = self.evaluate_internal(condition, data)?;
9344 let is_truthy = self.is_truthy(&cond_value);
9345
9346 if is_truthy {
9347 self.evaluate_for_tco(then_branch, data)
9348 } else if let Some(else_expr) = else_branch {
9349 self.evaluate_for_tco(else_expr, data)
9350 } else {
9351 Ok(LambdaResult::JValue(JValue::Null))
9352 }
9353 }
9354
9355 // Block: evaluate all but last normally, last for TCO
9356 AstNode::Block(exprs) => {
9357 if exprs.is_empty() {
9358 return Ok(LambdaResult::JValue(JValue::Null));
9359 }
9360
9361 // Evaluate all expressions except the last
9362 let mut result = JValue::Null;
9363 for (i, expr) in exprs.iter().enumerate() {
9364 if i == exprs.len() - 1 {
9365 // Last expression - evaluate for TCO
9366 return self.evaluate_for_tco(expr, data);
9367 } else {
9368 result = self.evaluate_internal(expr, data)?;
9369 }
9370 }
9371 Ok(LambdaResult::JValue(result))
9372 }
9373
9374 // Variable binding: evaluate value, bind, then evaluate result for TCO if present
9375 AstNode::Binary {
9376 op: BinaryOp::ColonEqual,
9377 lhs,
9378 rhs,
9379 } => {
9380 // This is var := value; get the variable name
9381 let var_name = match lhs.as_ref() {
9382 AstNode::Variable(name) => name.clone(),
9383 _ => {
9384 // Not a simple variable binding, evaluate normally
9385 let result = self.evaluate_internal(node, data)?;
9386 return Ok(LambdaResult::JValue(result));
9387 }
9388 };
9389
9390 // Check if RHS is a lambda - store it specially
9391 if let AstNode::Lambda {
9392 params,
9393 body,
9394 signature,
9395 thunk,
9396 } = rhs.as_ref()
9397 {
9398 let captured_env = self.capture_environment_for(body, params);
9399 let compiled_body = if !thunk {
9400 let var_refs: Vec<&str> = params.iter().map(|s| s.as_str()).collect();
9401 try_compile_expr_with_allowed_vars(body, &var_refs)
9402 } else {
9403 None
9404 };
9405 let stored_lambda = StoredLambda {
9406 params: params.clone(),
9407 body: (**body).clone(),
9408 compiled_body,
9409 signature: signature.clone(),
9410 captured_env,
9411 captured_data: Some(data.clone()),
9412 thunk: *thunk,
9413 };
9414 self.context.bind_lambda(var_name, stored_lambda);
9415 let lambda_repr =
9416 JValue::lambda("anon", params.clone(), None::<String>, None::<String>);
9417 return Ok(LambdaResult::JValue(lambda_repr));
9418 }
9419
9420 // Evaluate the RHS
9421 let value = self.evaluate_internal(rhs, data)?;
9422 self.context.bind(var_name, value.clone());
9423 Ok(LambdaResult::JValue(value))
9424 }
9425
9426 // Function call - this is where TCO happens
9427 AstNode::Function { name, args, .. } => {
9428 // Check if this is a call to a stored lambda (user function)
9429 if let Some(stored_lambda) = self.context.lookup_lambda(name).cloned() {
9430 if stored_lambda.thunk {
9431 let mut evaluated_args = Vec::with_capacity(args.len());
9432 for arg in args {
9433 evaluated_args.push(self.evaluate_internal(arg, data)?);
9434 }
9435 return Ok(LambdaResult::TailCall {
9436 lambda: Box::new(stored_lambda),
9437 args: evaluated_args,
9438 data: data.clone(),
9439 });
9440 }
9441 }
9442 // Not a thunk lambda - evaluate normally
9443 let result = self.evaluate_internal(node, data)?;
9444 Ok(LambdaResult::JValue(result))
9445 }
9446
9447 // Call node (calling a lambda value)
9448 AstNode::Call { procedure, args } => {
9449 // Evaluate the procedure to get the callable
9450 let callable = self.evaluate_internal(procedure, data)?;
9451
9452 // Check if it's a lambda with TCO
9453 if let JValue::Lambda { lambda_id, .. } = &callable {
9454 if let Some(stored_lambda) = self.context.lookup_lambda(lambda_id).cloned() {
9455 if stored_lambda.thunk {
9456 let mut evaluated_args = Vec::with_capacity(args.len());
9457 for arg in args {
9458 evaluated_args.push(self.evaluate_internal(arg, data)?);
9459 }
9460 return Ok(LambdaResult::TailCall {
9461 lambda: Box::new(stored_lambda),
9462 args: evaluated_args,
9463 data: data.clone(),
9464 });
9465 }
9466 }
9467 }
9468 // Not a thunk - evaluate normally
9469 let result = self.evaluate_internal(node, data)?;
9470 Ok(LambdaResult::JValue(result))
9471 }
9472
9473 // Variable reference that might be a function call
9474 // This handles cases like $f($x) where $f is referenced by name
9475 AstNode::Variable(_) => {
9476 let result = self.evaluate_internal(node, data)?;
9477 Ok(LambdaResult::JValue(result))
9478 }
9479
9480 // Any other expression - evaluate normally
9481 _ => {
9482 let result = self.evaluate_internal(node, data)?;
9483 Ok(LambdaResult::JValue(result))
9484 }
9485 }
9486 }
9487
9488 /// Match with custom matcher function
9489 ///
9490 /// Implements custom matcher support for $match(str, matcherFunction, limit?)
9491 /// The matcher function is called with the string and returns:
9492 /// { match: string, start: number, end: number, groups: [], next: function }
9493 /// The next function is called repeatedly to get subsequent matches
9494 fn match_with_custom_matcher(
9495 &mut self,
9496 str_value: &str,
9497 matcher_node: &AstNode,
9498 limit: Option<usize>,
9499 data: &JValue,
9500 ) -> Result<JValue, EvaluatorError> {
9501 let mut results = Vec::new();
9502 let mut count = 0;
9503
9504 // Call the matcher function with the string
9505 let str_val = JValue::string(str_value.to_string());
9506 let mut current_match = self.apply_function(matcher_node, &[str_val], data)?;
9507
9508 // Iterate through matches following the 'next' chain
9509 while !current_match.is_undefined() && !current_match.is_null() {
9510 // Check limit
9511 if let Some(lim) = limit {
9512 if count >= lim {
9513 break;
9514 }
9515 }
9516
9517 // Extract match information from the result object
9518 if let JValue::Object(ref match_obj) = current_match {
9519 // Validate that this is a proper match object
9520 let has_match = match_obj.contains_key("match");
9521 let has_start = match_obj.contains_key("start");
9522 let has_end = match_obj.contains_key("end");
9523 let has_groups = match_obj.contains_key("groups");
9524 let has_next = match_obj.contains_key("next");
9525
9526 if !has_match && !has_start && !has_end && !has_groups && !has_next {
9527 // Invalid matcher result - T1010 error
9528 return Err(EvaluatorError::EvaluationError(
9529 "T1010: The matcher function did not return the correct object structure"
9530 .to_string(),
9531 ));
9532 }
9533
9534 // Build the result match object (match, index, groups)
9535 let mut result_obj = IndexMap::new();
9536
9537 if let Some(match_val) = match_obj.get("match") {
9538 result_obj.insert("match".to_string(), match_val.clone());
9539 }
9540
9541 if let Some(start_val) = match_obj.get("start") {
9542 result_obj.insert("index".to_string(), start_val.clone());
9543 }
9544
9545 if let Some(groups_val) = match_obj.get("groups") {
9546 result_obj.insert("groups".to_string(), groups_val.clone());
9547 }
9548
9549 results.push(JValue::object(result_obj));
9550 count += 1;
9551
9552 // Get the next match by calling the 'next' function
9553 if let Some(next_func) = match_obj.get("next") {
9554 if let Some(stored) = self.lookup_lambda_from_value(next_func) {
9555 current_match = self.invoke_stored_lambda(&stored, &[], data)?;
9556 continue;
9557 }
9558 }
9559
9560 // No next function or couldn't call it - stop iteration
9561 break;
9562 } else {
9563 // Not a valid match object
9564 break;
9565 }
9566 }
9567
9568 // Return results
9569 if results.is_empty() {
9570 Ok(JValue::Undefined)
9571 } else {
9572 Ok(JValue::array(results))
9573 }
9574 }
9575
9576 /// Replace with lambda/function callback
9577 ///
9578 /// Implements lambda replacement for $replace(str, pattern, function, limit?)
9579 /// The function receives a match object with: match, start, end, groups
9580 fn replace_with_lambda(
9581 &mut self,
9582 str_value: &JValue,
9583 pattern_value: &JValue,
9584 lambda_value: &JValue,
9585 limit_value: Option<&JValue>,
9586 data: &JValue,
9587 ) -> Result<JValue, EvaluatorError> {
9588 // Extract string
9589 let s = match str_value {
9590 JValue::String(s) => &**s,
9591 _ => {
9592 return Err(EvaluatorError::TypeError(
9593 "replace() requires string arguments".to_string(),
9594 ))
9595 }
9596 };
9597
9598 // Extract regex pattern
9599 let (pattern, flags) =
9600 crate::functions::string::extract_regex(pattern_value).ok_or_else(|| {
9601 EvaluatorError::TypeError(
9602 "replace() pattern must be a regex when using lambda replacement".to_string(),
9603 )
9604 })?;
9605
9606 // Build regex
9607 let re = crate::functions::string::build_regex(&pattern, &flags)?;
9608
9609 // Parse limit
9610 let limit = if let Some(lim_val) = limit_value {
9611 match lim_val {
9612 JValue::Number(n) => {
9613 let lim_f64 = *n;
9614 if lim_f64 < 0.0 {
9615 return Err(EvaluatorError::EvaluationError(format!(
9616 "D3011: Limit must be non-negative, got {}",
9617 lim_f64
9618 )));
9619 }
9620 Some(lim_f64 as usize)
9621 }
9622 _ => {
9623 return Err(EvaluatorError::TypeError(
9624 "replace() limit must be a number".to_string(),
9625 ))
9626 }
9627 }
9628 } else {
9629 None
9630 };
9631
9632 // Iterate through matches and replace using lambda
9633 let mut result = String::new();
9634 let mut last_end = 0;
9635 let mut count = 0;
9636
9637 for cap in re.captures_iter(s) {
9638 // Check limit
9639 if let Some(lim) = limit {
9640 if count >= lim {
9641 break;
9642 }
9643 }
9644
9645 let m = cap.get(0).unwrap();
9646 let match_start = m.start();
9647 let match_end = m.end();
9648 let match_str = m.as_str();
9649
9650 // Add text before match
9651 result.push_str(&s[last_end..match_start]);
9652
9653 // Build match object
9654 let groups: Vec<JValue> = (1..cap.len())
9655 .map(|i| {
9656 cap.get(i)
9657 .map(|m| JValue::string(m.as_str().to_string()))
9658 .unwrap_or(JValue::Null)
9659 })
9660 .collect();
9661
9662 let mut match_map = IndexMap::new();
9663 match_map.insert("match".to_string(), JValue::string(match_str));
9664 match_map.insert("start".to_string(), JValue::Number(match_start as f64));
9665 match_map.insert("end".to_string(), JValue::Number(match_end as f64));
9666 match_map.insert("groups".to_string(), JValue::array(groups));
9667 let match_obj = JValue::object(match_map);
9668
9669 // Invoke lambda with match object
9670 let stored_lambda = self.lookup_lambda_from_value(lambda_value).ok_or_else(|| {
9671 EvaluatorError::TypeError("Replacement must be a lambda function".to_string())
9672 })?;
9673 let lambda_result = self.invoke_stored_lambda(&stored_lambda, &[match_obj], data)?;
9674 let replacement_str = match lambda_result {
9675 JValue::String(s) => s,
9676 _ => {
9677 return Err(EvaluatorError::TypeError(format!(
9678 "D3012: Replacement function must return a string, got {:?}",
9679 lambda_result
9680 )))
9681 }
9682 };
9683
9684 // Add replacement
9685 result.push_str(&replacement_str);
9686
9687 last_end = match_end;
9688 count += 1;
9689 }
9690
9691 // Add remaining text after last match
9692 result.push_str(&s[last_end..]);
9693
9694 Ok(JValue::string(result))
9695 }
9696
9697 /// Capture the current environment bindings for closure support
9698 fn capture_current_environment(&self) -> HashMap<String, JValue> {
9699 self.context.all_bindings()
9700 }
9701
9702 /// Capture only the variables referenced by a lambda body (selective capture).
9703 /// This avoids cloning the entire environment when only a few variables are needed.
9704 fn capture_environment_for(
9705 &self,
9706 body: &AstNode,
9707 params: &[String],
9708 ) -> HashMap<String, JValue> {
9709 let free_vars = Self::collect_free_variables(body, params);
9710 if free_vars.is_empty() {
9711 return HashMap::new();
9712 }
9713 let mut result = HashMap::new();
9714 for var_name in &free_vars {
9715 if let Some(value) = self.context.lookup(var_name) {
9716 result.insert(var_name.clone(), value.clone());
9717 }
9718 }
9719 result
9720 }
9721
9722 /// Collect all free variables in an AST node that are not bound by the given params.
9723 /// A "free variable" is one that is referenced but not defined within the expression.
9724 fn collect_free_variables(body: &AstNode, params: &[String]) -> HashSet<String> {
9725 let mut free_vars = HashSet::new();
9726 let bound: HashSet<&str> = params.iter().map(|s| s.as_str()).collect();
9727 Self::collect_free_vars_walk(body, &bound, &mut free_vars);
9728 free_vars
9729 }
9730
9731 fn collect_free_vars_walk(node: &AstNode, bound: &HashSet<&str>, free: &mut HashSet<String>) {
9732 match node {
9733 AstNode::Variable(name) => {
9734 if !bound.contains(name.as_str()) {
9735 free.insert(name.clone());
9736 }
9737 }
9738 AstNode::Function { name, args, .. } => {
9739 // Function name references a variable (e.g., $f(...))
9740 if !bound.contains(name.as_str()) {
9741 free.insert(name.clone());
9742 }
9743 for arg in args {
9744 Self::collect_free_vars_walk(arg, bound, free);
9745 }
9746 }
9747 AstNode::Lambda { params, body, .. } => {
9748 // Inner lambda introduces new bindings
9749 let mut inner_bound = bound.clone();
9750 for p in params {
9751 inner_bound.insert(p.as_str());
9752 }
9753 Self::collect_free_vars_walk(body, &inner_bound, free);
9754 }
9755 AstNode::Binary { op, lhs, rhs } => {
9756 Self::collect_free_vars_walk(lhs, bound, free);
9757 Self::collect_free_vars_walk(rhs, bound, free);
9758 // For ColonEqual, note: the binding is visible after this expr in blocks,
9759 // but block handling takes care of that separately
9760 let _ = op;
9761 }
9762 AstNode::Unary { operand, .. } => {
9763 Self::collect_free_vars_walk(operand, bound, free);
9764 }
9765 AstNode::Path { steps } => {
9766 for step in steps {
9767 Self::collect_free_vars_walk(&step.node, bound, free);
9768 for stage in &step.stages {
9769 match stage {
9770 Stage::Filter(expr) => Self::collect_free_vars_walk(expr, bound, free),
9771 // An index stage binds a variable; it introduces no
9772 // free variable references.
9773 Stage::Index(_) => {}
9774 }
9775 }
9776 }
9777 }
9778 AstNode::Call { procedure, args } => {
9779 Self::collect_free_vars_walk(procedure, bound, free);
9780 for arg in args {
9781 Self::collect_free_vars_walk(arg, bound, free);
9782 }
9783 }
9784 AstNode::Conditional {
9785 condition,
9786 then_branch,
9787 else_branch,
9788 } => {
9789 Self::collect_free_vars_walk(condition, bound, free);
9790 Self::collect_free_vars_walk(then_branch, bound, free);
9791 if let Some(else_expr) = else_branch {
9792 Self::collect_free_vars_walk(else_expr, bound, free);
9793 }
9794 }
9795 AstNode::Block(exprs) => {
9796 let mut block_bound = bound.clone();
9797 for expr in exprs {
9798 Self::collect_free_vars_walk(expr, &block_bound, free);
9799 // Bindings introduced via := become bound for subsequent expressions
9800 if let AstNode::Binary {
9801 op: BinaryOp::ColonEqual,
9802 lhs,
9803 ..
9804 } = expr
9805 {
9806 if let AstNode::Variable(var_name) = lhs.as_ref() {
9807 block_bound.insert(var_name.as_str());
9808 }
9809 }
9810 }
9811 }
9812 AstNode::Array(exprs) | AstNode::ArrayGroup(exprs) => {
9813 for expr in exprs {
9814 Self::collect_free_vars_walk(expr, bound, free);
9815 }
9816 }
9817 AstNode::Object(pairs) => {
9818 for (key, value) in pairs {
9819 Self::collect_free_vars_walk(key, bound, free);
9820 Self::collect_free_vars_walk(value, bound, free);
9821 }
9822 }
9823 AstNode::ObjectTransform { input, pattern } => {
9824 Self::collect_free_vars_walk(input, bound, free);
9825 for (key, value) in pattern {
9826 Self::collect_free_vars_walk(key, bound, free);
9827 Self::collect_free_vars_walk(value, bound, free);
9828 }
9829 }
9830 AstNode::Predicate(expr) | AstNode::FunctionApplication(expr) => {
9831 Self::collect_free_vars_walk(expr, bound, free);
9832 }
9833 AstNode::Sort { input, terms } => {
9834 Self::collect_free_vars_walk(input, bound, free);
9835 for (expr, _) in terms {
9836 Self::collect_free_vars_walk(expr, bound, free);
9837 }
9838 }
9839 AstNode::Transform {
9840 location,
9841 update,
9842 delete,
9843 } => {
9844 Self::collect_free_vars_walk(location, bound, free);
9845 Self::collect_free_vars_walk(update, bound, free);
9846 if let Some(del) = delete {
9847 Self::collect_free_vars_walk(del, bound, free);
9848 }
9849 }
9850 // Leaf nodes with no variable references
9851 AstNode::String(_)
9852 | AstNode::Name(_)
9853 | AstNode::Number(_)
9854 | AstNode::Boolean(_)
9855 | AstNode::Null
9856 | AstNode::Undefined
9857 | AstNode::Placeholder
9858 | AstNode::Regex { .. }
9859 | AstNode::Wildcard
9860 | AstNode::Descendant
9861 | AstNode::Parent(_)
9862 | AstNode::ParentVariable(_) => {}
9863 }
9864 }
9865
9866 /// Check if a name refers to a built-in function
9867 fn is_builtin_function(&self, name: &str) -> bool {
9868 matches!(
9869 name,
9870 // String functions
9871 "string" | "length" | "substring" | "substringBefore" | "substringAfter" |
9872 "uppercase" | "lowercase" | "trim" | "pad" | "contains" | "split" |
9873 "join" | "match" | "replace" | "eval" | "base64encode" | "base64decode" |
9874 "encodeUrlComponent" | "encodeUrl" | "decodeUrlComponent" | "decodeUrl" |
9875
9876 // Numeric functions
9877 "number" | "abs" | "floor" | "ceil" | "round" | "power" | "sqrt" |
9878 "random" | "formatNumber" | "formatBase" | "formatInteger" | "parseInteger" |
9879
9880 // Aggregation functions
9881 "sum" | "max" | "min" | "average" |
9882
9883 // Boolean/logic functions
9884 "boolean" | "not" | "exists" |
9885
9886 // Array functions
9887 "count" | "append" | "sort" | "reverse" | "shuffle" | "distinct" | "zip" |
9888
9889 // Object functions
9890 "keys" | "lookup" | "spread" | "merge" | "sift" | "each" | "error" | "assert" | "type" |
9891
9892 // Higher-order functions
9893 "map" | "filter" | "reduce" | "singletonArray" |
9894
9895 // Date/time functions
9896 "now" | "millis" | "fromMillis" | "toMillis"
9897 )
9898 }
9899
9900 /// Call a built-in function directly with pre-evaluated Values
9901 /// This is used when passing built-in functions to higher-order functions like $map
9902 fn call_builtin_with_values(
9903 &mut self,
9904 name: &str,
9905 values: &[JValue],
9906 ) -> Result<JValue, EvaluatorError> {
9907 use crate::functions;
9908
9909 if values.is_empty() {
9910 return Err(EvaluatorError::EvaluationError(format!(
9911 "{}() requires at least 1 argument",
9912 name
9913 )));
9914 }
9915
9916 let arg = &values[0];
9917
9918 match name {
9919 "string" => Ok(functions::string::string(arg, None)?),
9920 "number" => Ok(functions::numeric::number(arg)?),
9921 "boolean" => Ok(functions::boolean::boolean(arg)?),
9922 "not" => {
9923 let b = functions::boolean::boolean(arg)?;
9924 match b {
9925 JValue::Bool(val) => Ok(JValue::Bool(!val)),
9926 _ => Err(EvaluatorError::TypeError(
9927 "not() requires a boolean".to_string(),
9928 )),
9929 }
9930 }
9931 "exists" => Ok(JValue::Bool(!arg.is_null())),
9932 "abs" => match arg {
9933 JValue::Number(n) => Ok(functions::numeric::abs(*n)?),
9934 _ => Err(EvaluatorError::TypeError(
9935 "abs() requires a number argument".to_string(),
9936 )),
9937 },
9938 "floor" => match arg {
9939 JValue::Number(n) => Ok(functions::numeric::floor(*n)?),
9940 _ => Err(EvaluatorError::TypeError(
9941 "floor() requires a number argument".to_string(),
9942 )),
9943 },
9944 "ceil" => match arg {
9945 JValue::Number(n) => Ok(functions::numeric::ceil(*n)?),
9946 _ => Err(EvaluatorError::TypeError(
9947 "ceil() requires a number argument".to_string(),
9948 )),
9949 },
9950 "round" => match arg {
9951 JValue::Number(n) => Ok(functions::numeric::round(*n, None)?),
9952 _ => Err(EvaluatorError::TypeError(
9953 "round() requires a number argument".to_string(),
9954 )),
9955 },
9956 "sqrt" => match arg {
9957 JValue::Number(n) => Ok(functions::numeric::sqrt(*n)?),
9958 _ => Err(EvaluatorError::TypeError(
9959 "sqrt() requires a number argument".to_string(),
9960 )),
9961 },
9962 "uppercase" => match arg {
9963 JValue::String(s) => Ok(JValue::string(s.to_uppercase())),
9964 JValue::Null => Ok(JValue::Null),
9965 _ => Err(EvaluatorError::TypeError(
9966 "uppercase() requires a string argument".to_string(),
9967 )),
9968 },
9969 "lowercase" => match arg {
9970 JValue::String(s) => Ok(JValue::string(s.to_lowercase())),
9971 JValue::Null => Ok(JValue::Null),
9972 _ => Err(EvaluatorError::TypeError(
9973 "lowercase() requires a string argument".to_string(),
9974 )),
9975 },
9976 "trim" => match arg {
9977 JValue::String(s) => Ok(JValue::string(s.trim().to_string())),
9978 JValue::Null => Ok(JValue::Null),
9979 _ => Err(EvaluatorError::TypeError(
9980 "trim() requires a string argument".to_string(),
9981 )),
9982 },
9983 "length" => match arg {
9984 JValue::String(s) => Ok(JValue::Number(s.chars().count() as f64)),
9985 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
9986 JValue::Null => Ok(JValue::Null),
9987 _ => Err(EvaluatorError::TypeError(
9988 "length() requires a string or array argument".to_string(),
9989 )),
9990 },
9991 "sum" => match arg {
9992 JValue::Array(arr) => {
9993 let mut total = 0.0;
9994 for item in arr.iter() {
9995 match item {
9996 JValue::Number(n) => {
9997 total += *n;
9998 }
9999 _ => {
10000 return Err(EvaluatorError::TypeError(
10001 "sum() requires all array elements to be numbers".to_string(),
10002 ));
10003 }
10004 }
10005 }
10006 Ok(JValue::Number(total))
10007 }
10008 JValue::Number(n) => Ok(JValue::Number(*n)),
10009 JValue::Null => Ok(JValue::Null),
10010 _ => Err(EvaluatorError::TypeError(
10011 "sum() requires an array of numbers".to_string(),
10012 )),
10013 },
10014 "count" => {
10015 match arg {
10016 JValue::Array(arr) => Ok(JValue::Number(arr.len() as f64)),
10017 JValue::Null => Ok(JValue::Number(0.0)),
10018 _ => Ok(JValue::Number(1.0)), // Single value counts as 1
10019 }
10020 }
10021 "max" => match arg {
10022 JValue::Array(arr) => {
10023 let mut max_val: Option<f64> = None;
10024 for item in arr.iter() {
10025 if let JValue::Number(n) = item {
10026 let f = *n;
10027 max_val = Some(max_val.map_or(f, |m| m.max(f)));
10028 }
10029 }
10030 max_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10031 }
10032 JValue::Number(n) => Ok(JValue::Number(*n)),
10033 JValue::Null => Ok(JValue::Null),
10034 _ => Err(EvaluatorError::TypeError(
10035 "max() requires an array of numbers".to_string(),
10036 )),
10037 },
10038 "min" => match arg {
10039 JValue::Array(arr) => {
10040 let mut min_val: Option<f64> = None;
10041 for item in arr.iter() {
10042 if let JValue::Number(n) = item {
10043 let f = *n;
10044 min_val = Some(min_val.map_or(f, |m| m.min(f)));
10045 }
10046 }
10047 min_val.map_or(Ok(JValue::Null), |m| Ok(JValue::Number(m)))
10048 }
10049 JValue::Number(n) => Ok(JValue::Number(*n)),
10050 JValue::Null => Ok(JValue::Null),
10051 _ => Err(EvaluatorError::TypeError(
10052 "min() requires an array of numbers".to_string(),
10053 )),
10054 },
10055 "average" => match arg {
10056 JValue::Array(arr) => {
10057 let nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
10058 if nums.is_empty() {
10059 Ok(JValue::Null)
10060 } else {
10061 let avg = nums.iter().sum::<f64>() / nums.len() as f64;
10062 Ok(JValue::Number(avg))
10063 }
10064 }
10065 JValue::Number(n) => Ok(JValue::Number(*n)),
10066 JValue::Null => Ok(JValue::Null),
10067 _ => Err(EvaluatorError::TypeError(
10068 "average() requires an array of numbers".to_string(),
10069 )),
10070 },
10071 "append" => {
10072 // append(array1, array2) - append second array to first
10073 if values.len() < 2 {
10074 return Err(EvaluatorError::EvaluationError(
10075 "append() requires 2 arguments".to_string(),
10076 ));
10077 }
10078 let first = &values[0];
10079 let second = &values[1];
10080
10081 // Convert first to array if needed
10082 let mut result = match first {
10083 JValue::Array(arr) => arr.to_vec(),
10084 JValue::Null => vec![],
10085 other => vec![other.clone()],
10086 };
10087
10088 // Append second (flatten if array)
10089 match second {
10090 JValue::Array(arr) => result.extend(arr.iter().cloned()),
10091 JValue::Null => {}
10092 other => result.push(other.clone()),
10093 }
10094
10095 Ok(JValue::array(result))
10096 }
10097 "reverse" => match arg {
10098 JValue::Array(arr) => {
10099 let mut reversed = arr.to_vec();
10100 reversed.reverse();
10101 Ok(JValue::array(reversed))
10102 }
10103 JValue::Null => Ok(JValue::Null),
10104 _ => Err(EvaluatorError::TypeError(
10105 "reverse() requires an array".to_string(),
10106 )),
10107 },
10108 "keys" => match arg {
10109 JValue::Object(obj) => {
10110 let keys: Vec<JValue> = obj.keys().map(|k| JValue::string(k.clone())).collect();
10111 Ok(JValue::array(keys))
10112 }
10113 JValue::Null => Ok(JValue::Null),
10114 _ => Err(EvaluatorError::TypeError(
10115 "keys() requires an object".to_string(),
10116 )),
10117 },
10118
10119 // Add more functions as needed
10120 _ => Err(EvaluatorError::ReferenceError(format!(
10121 "Built-in function {} cannot be called with values directly",
10122 name
10123 ))),
10124 }
10125 }
10126
10127 /// Collect all descendant values recursively
10128 fn collect_descendants(&self, value: &JValue) -> Vec<JValue> {
10129 let mut descendants = Vec::new();
10130
10131 match value {
10132 JValue::Null => {
10133 // Null has no descendants, return empty
10134 return descendants;
10135 }
10136 JValue::Object(obj) => {
10137 // Include the current object
10138 descendants.push(value.clone());
10139
10140 for val in obj.values() {
10141 // Recursively collect descendants
10142 descendants.extend(self.collect_descendants(val));
10143 }
10144 }
10145 JValue::Array(arr) => {
10146 // DO NOT include the array itself - only recurse into elements
10147 // This matches JavaScript behavior: arrays are traversed but not collected
10148 for val in arr.iter() {
10149 // Recursively collect descendants
10150 descendants.extend(self.collect_descendants(val));
10151 }
10152 }
10153 _ => {
10154 // For primitives (string, number, boolean), just include the value itself
10155 descendants.push(value.clone());
10156 }
10157 }
10158
10159 descendants
10160 }
10161
10162 /// Evaluate a predicate (array filter or index)
10163 fn evaluate_predicate(
10164 &mut self,
10165 current: &JValue,
10166 predicate: &AstNode,
10167 ) -> Result<JValue, EvaluatorError> {
10168 // Special case: empty brackets [] (represented as Boolean(true))
10169 // This forces the value to be wrapped in an array
10170 if matches!(predicate, AstNode::Boolean(true)) {
10171 return match current {
10172 JValue::Array(arr) => Ok(JValue::Array(arr.clone())),
10173 JValue::Null => Ok(JValue::Null),
10174 other => Ok(JValue::array(vec![other.clone()])),
10175 };
10176 }
10177
10178 match current {
10179 JValue::Array(_arr) => {
10180 // Standalone predicates do simple array operations (no mapping over sub-arrays)
10181
10182 // First, try to evaluate predicate as a simple number (array index)
10183 if let AstNode::Number(n) = predicate {
10184 // Direct array indexing
10185 return self.array_index(current, &JValue::Number(*n));
10186 }
10187
10188 // Fast path: if predicate is definitely a filter expression (comparison/logical),
10189 // skip speculative numeric evaluation and go directly to filter logic
10190 if Self::is_filter_predicate(predicate) {
10191 // Try CompiledExpr fast path
10192 if let Some(compiled) = try_compile_expr(predicate) {
10193 let shape = _arr.first().and_then(build_shape_cache);
10194 let mut filtered = Vec::with_capacity(_arr.len());
10195 for item in _arr.iter() {
10196 let result = if let Some(ref s) = shape {
10197 eval_compiled_shaped(&compiled, item, None, s)?
10198 } else {
10199 eval_compiled(&compiled, item, None)?
10200 };
10201 if compiled_is_truthy(&result) {
10202 filtered.push(item.clone());
10203 }
10204 }
10205 return Ok(JValue::array(filtered));
10206 }
10207 // Fallback: full AST evaluation per element
10208 let mut filtered = Vec::new();
10209 for item in _arr.iter() {
10210 let item_result = self.evaluate_internal(predicate, item)?;
10211 if self.is_truthy(&item_result) {
10212 filtered.push(item.clone());
10213 }
10214 }
10215 return Ok(JValue::array(filtered));
10216 }
10217
10218 // Try to evaluate the predicate to see if it's a numeric index
10219 // If evaluation succeeds and yields a number, use it as an index
10220 // If evaluation fails (e.g., comparison error), treat as filter
10221 match self.evaluate_internal(predicate, current) {
10222 Ok(JValue::Number(_)) => {
10223 // It's a numeric index
10224 let pred_result = self.evaluate_internal(predicate, current)?;
10225 return self.array_index(current, &pred_result);
10226 }
10227 Ok(JValue::Array(indices)) => {
10228 // Multiple array selectors [[indices]]
10229 // Check if array contains any non-numeric values
10230 let has_non_numeric =
10231 indices.iter().any(|v| !matches!(v, JValue::Number(_)));
10232
10233 if has_non_numeric {
10234 // If array contains non-numeric values, return entire array
10235 return Ok(current.clone());
10236 }
10237
10238 // Collect numeric indices, handling negative indices
10239 let arr_len = _arr.len() as i64;
10240 let mut resolved_indices: Vec<i64> = indices
10241 .iter()
10242 .filter_map(|v| {
10243 if let JValue::Number(n) = v {
10244 let idx = *n as i64;
10245 // Resolve negative indices
10246 let actual_idx = if idx < 0 { arr_len + idx } else { idx };
10247 // Only include valid indices
10248 if actual_idx >= 0 && actual_idx < arr_len {
10249 Some(actual_idx)
10250 } else {
10251 None
10252 }
10253 } else {
10254 None
10255 }
10256 })
10257 .collect();
10258
10259 // Sort and deduplicate indices
10260 resolved_indices.sort();
10261 resolved_indices.dedup();
10262
10263 // Select elements at each sorted index
10264 let result: Vec<JValue> = resolved_indices
10265 .iter()
10266 .map(|&idx| _arr[idx as usize].clone())
10267 .collect();
10268
10269 return Ok(JValue::array(result));
10270 }
10271 Ok(_) => {
10272 // Evaluated successfully but not a number - might be a filter
10273 // Fall through to filter logic
10274 }
10275 Err(_) => {
10276 // Evaluation failed - it's likely a filter expression
10277 // Fall through to filter logic
10278 }
10279 }
10280
10281 // Try CompiledExpr fast path for filter expressions
10282 if let Some(compiled) = try_compile_expr(predicate) {
10283 let shape = _arr.first().and_then(build_shape_cache);
10284 let mut filtered = Vec::with_capacity(_arr.len());
10285 for item in _arr.iter() {
10286 let result = if let Some(ref s) = shape {
10287 eval_compiled_shaped(&compiled, item, None, s)?
10288 } else {
10289 eval_compiled(&compiled, item, None)?
10290 };
10291 if compiled_is_truthy(&result) {
10292 filtered.push(item.clone());
10293 }
10294 }
10295 return Ok(JValue::array(filtered));
10296 }
10297
10298 // It's a filter expression - evaluate the predicate for each array element
10299 let mut filtered = Vec::new();
10300 for item in _arr.iter() {
10301 let item_result = self.evaluate_internal(predicate, item)?;
10302
10303 // If result is truthy, include this item
10304 if self.is_truthy(&item_result) {
10305 filtered.push(item.clone());
10306 }
10307 }
10308
10309 Ok(JValue::array(filtered))
10310 }
10311 JValue::Object(obj) => {
10312 // For objects, predicate can be either:
10313 // 1. A string - property access (computed property name)
10314 // 2. A boolean expression - filter (return object if truthy)
10315 let pred_result = self.evaluate_internal(predicate, current)?;
10316
10317 // If it's a string, use it as a key for property access
10318 if let JValue::String(key) = &pred_result {
10319 return Ok(obj.get(&**key).cloned().unwrap_or(JValue::Null));
10320 }
10321
10322 // Otherwise, treat as a filter expression
10323 // If the predicate is truthy, return the object; otherwise return undefined
10324 if self.is_truthy(&pred_result) {
10325 Ok(current.clone())
10326 } else {
10327 Ok(JValue::Undefined)
10328 }
10329 }
10330 _ => {
10331 // For primitive values (string, number, boolean):
10332 // In JSONata, scalars are treated as single-element arrays when indexed.
10333 // So value[0] returns value, value[1] returns undefined.
10334
10335 // First check if predicate is a numeric literal
10336 if let AstNode::Number(n) = predicate {
10337 // For scalars, index 0 or -1 returns the value, others return undefined
10338 let idx = n.floor() as i64;
10339 if idx == 0 || idx == -1 {
10340 return Ok(current.clone());
10341 } else {
10342 return Ok(JValue::Undefined);
10343 }
10344 }
10345
10346 // Try to evaluate the predicate to see if it's a numeric index
10347 let pred_result = self.evaluate_internal(predicate, current)?;
10348
10349 if let JValue::Number(n) = &pred_result {
10350 // It's a numeric index - treat scalar as single-element array
10351 let idx = n.floor() as i64;
10352 if idx == 0 || idx == -1 {
10353 return Ok(current.clone());
10354 } else {
10355 return Ok(JValue::Undefined);
10356 }
10357 }
10358
10359 // For non-numeric predicates, treat as a filter:
10360 // value[true] returns value, value[false] returns undefined
10361 // This enables patterns like: $k[$v>2] which returns $k if $v>2, otherwise undefined
10362 if self.is_truthy(&pred_result) {
10363 Ok(current.clone())
10364 } else {
10365 // Return undefined (not null) so $map can filter it out
10366 Ok(JValue::Undefined)
10367 }
10368 }
10369 }
10370 }
10371
10372 /// Evaluate a sort term expression, distinguishing missing fields from explicit null
10373 /// Returns JValue::Undefined for missing fields, JValue::Null for explicit null
10374 fn evaluate_sort_term(
10375 &mut self,
10376 term_expr: &AstNode,
10377 element: &JValue,
10378 ) -> Result<JValue, EvaluatorError> {
10379 // For tuples (from index binding), extract the actual value from @ field
10380 let actual_element = if let JValue::Object(obj) = element {
10381 if obj.get("__tuple__") == Some(&JValue::Bool(true)) {
10382 obj.get("@").cloned().unwrap_or(JValue::Null)
10383 } else {
10384 element.clone()
10385 }
10386 } else {
10387 element.clone()
10388 };
10389
10390 // For simple field access (Path with single Name step), check if field exists
10391 if let AstNode::Path { steps } = term_expr {
10392 if steps.len() == 1 && steps[0].stages.is_empty() {
10393 if let AstNode::Name(field_name) = &steps[0].node {
10394 // Check if the field exists in the element
10395 if let JValue::Object(obj) = &actual_element {
10396 return match obj.get(field_name) {
10397 Some(val) => Ok(val.clone()), // Field exists (may be null)
10398 None => Ok(JValue::Undefined), // Field is missing
10399 };
10400 } else {
10401 // Not an object - return undefined
10402 return Ok(JValue::Undefined);
10403 }
10404 }
10405 }
10406 }
10407
10408 // For complex expressions, evaluate against the tuple's `@` value (the
10409 // real element), not the wrapper. The tuple's carried focus/index/ancestor
10410 // bindings are reachable via context (bound by evaluate_sort), so a term
10411 // like `$`, `%.Price`, or `$pos` still resolves correctly.
10412 let result = self.evaluate_internal(term_expr, &actual_element)?;
10413
10414 // If the result is null from a complex expression, we can't easily tell if it's
10415 // "missing field" or "explicit null". For now, treat null results as undefined
10416 // to maintain compatibility with existing tests.
10417 // TODO: For full JS compatibility, would need deeper analysis of the expression
10418 if result.is_null() {
10419 return Ok(JValue::Undefined);
10420 }
10421
10422 Ok(result)
10423 }
10424
10425 /// Evaluate sort operator
10426 fn evaluate_sort(
10427 &mut self,
10428 data: &JValue,
10429 terms: &[(AstNode, bool)],
10430 ) -> Result<JValue, EvaluatorError> {
10431 // If data is null, return null
10432 if data.is_null() {
10433 return Ok(JValue::Null);
10434 }
10435
10436 // If data is not an array, return it as-is (can't sort a single value)
10437 let array = match data {
10438 JValue::Array(arr) => arr.clone(),
10439 other => return Ok(other.clone()),
10440 };
10441
10442 // If empty array, return as-is
10443 if array.is_empty() {
10444 return Ok(JValue::Array(array));
10445 }
10446
10447 // Evaluate sort keys for each element
10448 let mut indexed_array: Vec<(usize, Vec<JValue>)> = Vec::new();
10449
10450 for (idx, element) in array.iter().enumerate() {
10451 let mut sort_keys = Vec::new();
10452
10453 // When sorting a tuple stream (the input path had a `%`/`@`/`#`
10454 // step, so each element is a `{@, !label, $var, __tuple__}`
10455 // wrapper), bind its carried ancestor/focus/index keys into scope
10456 // so a `%` (or `$focus`) inside a sort term resolves -- mirroring
10457 // create_tuple_stream's per-tuple frame binding. Sort terms attach
10458 // to a synthetic step after the last input step, so `%` refers to
10459 // the last input step's ancestry, carried under `!label` here.
10460 // Saves/restores rather than blindly unbinding, so a tuple key
10461 // that collides with a live outer `:=` binding doesn't get
10462 // deleted once this row's sort terms are evaluated.
10463 let tuple_bindings = match element {
10464 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
10465 Some(self.bind_tuple_keys(obj))
10466 }
10467 _ => None,
10468 };
10469
10470 // When sorting a tuple stream, `$` and the term's data context are the
10471 // tuple's `@` value, not the `{@, $var, !label, __tuple__}` wrapper --
10472 // otherwise a term like `^($)` would try to order by the wrapper
10473 // object and raise T2008. The carried focus/index/ancestor keys stay
10474 // reachable via the context bindings established just above.
10475 let term_data = match element {
10476 JValue::Object(obj) if obj.get("__tuple__") == Some(&JValue::Bool(true)) => {
10477 obj.get("@").cloned().unwrap_or(JValue::Null)
10478 }
10479 other => other.clone(),
10480 };
10481
10482 // Evaluate each sort term with $ bound to the element
10483 for (term_expr, _ascending) in terms {
10484 // Save current $ binding
10485 let saved_dollar = self.context.lookup("$").cloned();
10486
10487 // Bind $ to current element
10488 self.context.bind("$".to_string(), term_data.clone());
10489
10490 // Evaluate the sort expression, distinguishing missing fields from explicit null
10491 let sort_value = self.evaluate_sort_term(term_expr, element)?;
10492
10493 // Restore $ binding
10494 if let Some(val) = saved_dollar {
10495 self.context.bind("$".to_string(), val);
10496 } else {
10497 self.context.unbind("$");
10498 }
10499
10500 sort_keys.push(sort_value);
10501 }
10502
10503 if let Some(tuple_bindings) = tuple_bindings {
10504 tuple_bindings.restore(self);
10505 }
10506
10507 indexed_array.push((idx, sort_keys));
10508 }
10509
10510 // Validate that all sort keys are comparable (same type, or undefined)
10511 // Undefined values (missing fields) are allowed and sort to the end
10512 // Null values (explicit null in data) are NOT allowed (typeof null === 'object' in JS, triggers T2008)
10513 for term_idx in 0..terms.len() {
10514 let mut first_valid_type: Option<&str> = None;
10515
10516 for (_idx, sort_keys) in &indexed_array {
10517 let sort_value = &sort_keys[term_idx];
10518
10519 // Skip undefined markers (missing fields) - these are allowed and sort to end
10520 if sort_value.is_undefined() {
10521 continue;
10522 }
10523
10524 // Get the type name for this value
10525 // Note: explicit null is NOT allowed - typeof null === 'object' in JS
10526 let value_type = match sort_value {
10527 JValue::Number(_) => "number",
10528 JValue::String(_) => "string",
10529 JValue::Bool(_) => "boolean",
10530 JValue::Array(_) => "array",
10531 JValue::Object(_) => "object", // This catches non-undefined objects
10532 JValue::Null => "null", // Explicit null from data
10533 _ => "unknown",
10534 };
10535
10536 // Check that sort keys are only numbers or strings
10537 // Null, boolean, array, and object types are not valid for sorting
10538 if value_type != "number" && value_type != "string" {
10539 return Err(EvaluatorError::TypeError("T2008: The expressions within an order-by clause must evaluate to numeric or string values".to_string()));
10540 }
10541
10542 // Check if this matches the first valid type we saw
10543 if let Some(first_type) = first_valid_type {
10544 if first_type != value_type {
10545 return Err(EvaluatorError::TypeError(format!(
10546 "T2007: Type mismatch when comparing values in order-by clause: {} and {}",
10547 first_type, value_type
10548 )));
10549 }
10550 } else {
10551 first_valid_type = Some(value_type);
10552 }
10553 }
10554 }
10555
10556 // Sort the indexed array
10557 indexed_array.sort_by(|a, b| {
10558 // Compare sort keys in order
10559 for (i, (_term_expr, ascending)) in terms.iter().enumerate() {
10560 let left = &a.1[i];
10561 let right = &b.1[i];
10562
10563 let cmp = self.compare_values(left, right);
10564
10565 if cmp != std::cmp::Ordering::Equal {
10566 return if *ascending { cmp } else { cmp.reverse() };
10567 }
10568 }
10569
10570 // If all keys are equal, maintain original order (stable sort)
10571 a.0.cmp(&b.0)
10572 });
10573
10574 // Extract sorted elements
10575 let sorted: Vec<JValue> = indexed_array
10576 .iter()
10577 .map(|(idx, _)| array[*idx].clone())
10578 .collect();
10579
10580 Ok(JValue::array(sorted))
10581 }
10582
10583 /// Compare two values for sorting (JSONata semantics)
10584 fn compare_values(&self, left: &JValue, right: &JValue) -> Ordering {
10585 // Handle undefined markers first - they sort to the end
10586 let left_undef = left.is_undefined();
10587 let right_undef = right.is_undefined();
10588
10589 if left_undef && right_undef {
10590 return Ordering::Equal;
10591 }
10592 if left_undef {
10593 return Ordering::Greater; // Undefined sorts last
10594 }
10595 if right_undef {
10596 return Ordering::Less;
10597 }
10598
10599 match (left, right) {
10600 // Nulls also sort last (explicit null in data)
10601 (JValue::Null, JValue::Null) => Ordering::Equal,
10602 (JValue::Null, _) => Ordering::Greater,
10603 (_, JValue::Null) => Ordering::Less,
10604
10605 // Numbers
10606 (JValue::Number(a), JValue::Number(b)) => {
10607 let a_f64 = *a;
10608 let b_f64 = *b;
10609 a_f64.partial_cmp(&b_f64).unwrap_or(Ordering::Equal)
10610 }
10611
10612 // Strings
10613 (JValue::String(a), JValue::String(b)) => a.cmp(b),
10614
10615 // Booleans
10616 (JValue::Bool(a), JValue::Bool(b)) => a.cmp(b),
10617
10618 // Arrays (lexicographic comparison)
10619 (JValue::Array(a), JValue::Array(b)) => {
10620 for (a_elem, b_elem) in a.iter().zip(b.iter()) {
10621 let cmp = self.compare_values(a_elem, b_elem);
10622 if cmp != Ordering::Equal {
10623 return cmp;
10624 }
10625 }
10626 a.len().cmp(&b.len())
10627 }
10628
10629 // Different types: use type ordering
10630 // null < bool < number < string < array < object
10631 (JValue::Bool(_), JValue::Number(_)) => Ordering::Less,
10632 (JValue::Bool(_), JValue::String(_)) => Ordering::Less,
10633 (JValue::Bool(_), JValue::Array(_)) => Ordering::Less,
10634 (JValue::Bool(_), JValue::Object(_)) => Ordering::Less,
10635
10636 (JValue::Number(_), JValue::Bool(_)) => Ordering::Greater,
10637 (JValue::Number(_), JValue::String(_)) => Ordering::Less,
10638 (JValue::Number(_), JValue::Array(_)) => Ordering::Less,
10639 (JValue::Number(_), JValue::Object(_)) => Ordering::Less,
10640
10641 (JValue::String(_), JValue::Bool(_)) => Ordering::Greater,
10642 (JValue::String(_), JValue::Number(_)) => Ordering::Greater,
10643 (JValue::String(_), JValue::Array(_)) => Ordering::Less,
10644 (JValue::String(_), JValue::Object(_)) => Ordering::Less,
10645
10646 (JValue::Array(_), JValue::Bool(_)) => Ordering::Greater,
10647 (JValue::Array(_), JValue::Number(_)) => Ordering::Greater,
10648 (JValue::Array(_), JValue::String(_)) => Ordering::Greater,
10649 (JValue::Array(_), JValue::Object(_)) => Ordering::Less,
10650
10651 (JValue::Object(_), _) => Ordering::Greater,
10652 _ => Ordering::Equal,
10653 }
10654 }
10655
10656 /// Check if a value is truthy (JSONata semantics).
10657 fn is_truthy(&self, value: &JValue) -> bool {
10658 match value {
10659 JValue::Null | JValue::Undefined => false,
10660 JValue::Bool(b) => *b,
10661 JValue::Number(n) => *n != 0.0,
10662 JValue::String(s) => !s.is_empty(),
10663 JValue::Array(arr) => !arr.is_empty(),
10664 JValue::Object(obj) => !obj.is_empty(),
10665 _ => false,
10666 }
10667 }
10668
10669 /// Check if a value is truthy for the default operator (?:)
10670 /// This has special semantics:
10671 /// - Lambda/function objects are not values, so they're falsy
10672 /// - Arrays containing only falsy elements are falsy
10673 /// - Otherwise, use standard truthiness
10674 fn is_truthy_for_default(&self, value: &JValue) -> bool {
10675 match value {
10676 // Lambda/function values are not data values, so they're falsy
10677 JValue::Lambda { .. } | JValue::Builtin { .. } => false,
10678 // Arrays need special handling - check if all elements are falsy
10679 JValue::Array(arr) => {
10680 if arr.is_empty() {
10681 return false;
10682 }
10683 // Array is truthy only if it contains at least one truthy element
10684 arr.iter().any(|elem| self.is_truthy(elem))
10685 }
10686 // For all other types, use standard truthiness
10687 _ => self.is_truthy(value),
10688 }
10689 }
10690
10691 /// Unwrap singleton arrays to scalar values
10692 /// This is used when no explicit array-keeping operation (like []) was used
10693 fn unwrap_singleton(&self, value: JValue) -> JValue {
10694 match value {
10695 JValue::Array(ref arr) if arr.len() == 1 => arr[0].clone(),
10696 _ => value,
10697 }
10698 }
10699
10700 /// Extract lambda IDs from a value (used for closure preservation)
10701 /// Finds any lambda_id references in the value so they can be preserved
10702 /// when exiting a block scope
10703 fn extract_lambda_ids(&self, value: &JValue) -> Vec<String> {
10704 // Fast path: scalars can never contain lambda references
10705 match value {
10706 JValue::Number(_)
10707 | JValue::Bool(_)
10708 | JValue::String(_)
10709 | JValue::Null
10710 | JValue::Undefined
10711 | JValue::Regex { .. }
10712 | JValue::Builtin { .. } => return Vec::new(),
10713 _ => {}
10714 }
10715 let mut ids = Vec::new();
10716 self.collect_lambda_ids(value, &mut ids);
10717 ids
10718 }
10719
10720 fn collect_lambda_ids(&self, value: &JValue, ids: &mut Vec<String>) {
10721 match value {
10722 JValue::Lambda { lambda_id, .. } => {
10723 let id_str = lambda_id.to_string();
10724 if !ids.contains(&id_str) {
10725 ids.push(id_str);
10726 // Transitively follow the stored lambda's captured_env
10727 // to find all referenced lambdas. This is critical for
10728 // closures like the Y-combinator where returned lambdas
10729 // capture other lambdas in their environment.
10730 if let Some(stored) = self.context.lookup_lambda(lambda_id) {
10731 let env_values: Vec<JValue> =
10732 stored.captured_env.values().cloned().collect();
10733 for env_value in &env_values {
10734 self.collect_lambda_ids(env_value, ids);
10735 }
10736 }
10737 }
10738 }
10739 JValue::Object(map) => {
10740 // Recurse into object values
10741 for v in map.values() {
10742 self.collect_lambda_ids(v, ids);
10743 }
10744 }
10745 JValue::Array(arr) => {
10746 // Recurse into array elements
10747 for v in arr.iter() {
10748 self.collect_lambda_ids(v, ids);
10749 }
10750 }
10751 _ => {}
10752 }
10753 }
10754
10755 /// Equality comparison (JSONata semantics)
10756 fn equals(&self, left: &JValue, right: &JValue) -> bool {
10757 crate::functions::array::values_equal(left, right)
10758 }
10759
10760 /// Addition
10761 fn add(
10762 &self,
10763 left: &JValue,
10764 right: &JValue,
10765 left_is_explicit_null: bool,
10766 right_is_explicit_null: bool,
10767 ) -> Result<JValue, EvaluatorError> {
10768 match (left, right) {
10769 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a + *b)),
10770 // Explicit null literal with number -> T2002 error
10771 (JValue::Null, JValue::Number(_)) if left_is_explicit_null => {
10772 Err(EvaluatorError::TypeError(
10773 "T2002: The left side of the + operator must evaluate to a number".to_string(),
10774 ))
10775 }
10776 (JValue::Number(_), JValue::Null) if right_is_explicit_null => {
10777 Err(EvaluatorError::TypeError(
10778 "T2002: The right side of the + operator must evaluate to a number".to_string(),
10779 ))
10780 }
10781 (JValue::Null, JValue::Null) if left_is_explicit_null || right_is_explicit_null => {
10782 Err(EvaluatorError::TypeError(
10783 "T2002: The left side of the + operator must evaluate to a number".to_string(),
10784 ))
10785 }
10786 // Undefined variable (null/undefined) with number -> undefined result
10787 (JValue::Null | JValue::Undefined, JValue::Number(_))
10788 | (JValue::Number(_), JValue::Null | JValue::Undefined) => Ok(JValue::Null),
10789 // Boolean with anything (including undefined) -> T2001 error
10790 (JValue::Bool(_), _) => Err(EvaluatorError::TypeError(
10791 "T2001: The left side of the '+' operator must evaluate to a number or a string"
10792 .to_string(),
10793 )),
10794 (_, JValue::Bool(_)) => Err(EvaluatorError::TypeError(
10795 "T2001: The right side of the '+' operator must evaluate to a number or a string"
10796 .to_string(),
10797 )),
10798 // Undefined with undefined -> undefined
10799 (JValue::Null | JValue::Undefined, JValue::Null | JValue::Undefined) => {
10800 Ok(JValue::Null)
10801 }
10802 _ => Err(EvaluatorError::TypeError(format!(
10803 "Cannot add {:?} and {:?}",
10804 left, right
10805 ))),
10806 }
10807 }
10808
10809 /// Subtraction
10810 fn subtract(
10811 &self,
10812 left: &JValue,
10813 right: &JValue,
10814 left_is_explicit_null: bool,
10815 right_is_explicit_null: bool,
10816 ) -> Result<JValue, EvaluatorError> {
10817 match (left, right) {
10818 (JValue::Number(a), JValue::Number(b)) => Ok(JValue::Number(*a - *b)),
10819 // Explicit null literal -> error
10820 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10821 "T2002: The left side of the - operator must evaluate to a number".to_string(),
10822 )),
10823 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10824 "T2002: The right side of the - operator must evaluate to a number".to_string(),
10825 )),
10826 // Undefined variables -> undefined result
10827 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10828 Ok(JValue::Null)
10829 }
10830 _ => Err(EvaluatorError::TypeError(format!(
10831 "Cannot subtract {:?} and {:?}",
10832 left, right
10833 ))),
10834 }
10835 }
10836
10837 /// Multiplication
10838 fn multiply(
10839 &self,
10840 left: &JValue,
10841 right: &JValue,
10842 left_is_explicit_null: bool,
10843 right_is_explicit_null: bool,
10844 ) -> Result<JValue, EvaluatorError> {
10845 match (left, right) {
10846 (JValue::Number(a), JValue::Number(b)) => {
10847 let result = *a * *b;
10848 // Check for overflow to Infinity
10849 if result.is_infinite() {
10850 return Err(EvaluatorError::EvaluationError(
10851 "D1001: Number out of range".to_string(),
10852 ));
10853 }
10854 Ok(JValue::Number(result))
10855 }
10856 // Explicit null literal -> error
10857 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10858 "T2002: The left side of the * operator must evaluate to a number".to_string(),
10859 )),
10860 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10861 "T2002: The right side of the * operator must evaluate to a number".to_string(),
10862 )),
10863 // Undefined variables -> undefined result
10864 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10865 Ok(JValue::Null)
10866 }
10867 _ => Err(EvaluatorError::TypeError(format!(
10868 "Cannot multiply {:?} and {:?}",
10869 left, right
10870 ))),
10871 }
10872 }
10873
10874 /// Division
10875 fn divide(
10876 &self,
10877 left: &JValue,
10878 right: &JValue,
10879 left_is_explicit_null: bool,
10880 right_is_explicit_null: bool,
10881 ) -> Result<JValue, EvaluatorError> {
10882 match (left, right) {
10883 (JValue::Number(a), JValue::Number(b)) => {
10884 let denominator = *b;
10885 if denominator == 0.0 {
10886 return Err(EvaluatorError::EvaluationError(
10887 "Division by zero".to_string(),
10888 ));
10889 }
10890 Ok(JValue::Number(*a / denominator))
10891 }
10892 // Explicit null literal -> error
10893 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10894 "T2002: The left side of the / operator must evaluate to a number".to_string(),
10895 )),
10896 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10897 "T2002: The right side of the / operator must evaluate to a number".to_string(),
10898 )),
10899 // Undefined variables -> undefined result
10900 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10901 Ok(JValue::Null)
10902 }
10903 _ => Err(EvaluatorError::TypeError(format!(
10904 "Cannot divide {:?} and {:?}",
10905 left, right
10906 ))),
10907 }
10908 }
10909
10910 /// Modulo
10911 fn modulo(
10912 &self,
10913 left: &JValue,
10914 right: &JValue,
10915 left_is_explicit_null: bool,
10916 right_is_explicit_null: bool,
10917 ) -> Result<JValue, EvaluatorError> {
10918 match (left, right) {
10919 (JValue::Number(a), JValue::Number(b)) => {
10920 let denominator = *b;
10921 if denominator == 0.0 {
10922 return Err(EvaluatorError::EvaluationError(
10923 "Division by zero".to_string(),
10924 ));
10925 }
10926 Ok(JValue::Number(*a % denominator))
10927 }
10928 // Explicit null literal -> error
10929 (JValue::Null, _) if left_is_explicit_null => Err(EvaluatorError::TypeError(
10930 "T2002: The left side of the % operator must evaluate to a number".to_string(),
10931 )),
10932 (_, JValue::Null) if right_is_explicit_null => Err(EvaluatorError::TypeError(
10933 "T2002: The right side of the % operator must evaluate to a number".to_string(),
10934 )),
10935 // Undefined variables -> undefined result
10936 (JValue::Null | JValue::Undefined, _) | (_, JValue::Null | JValue::Undefined) => {
10937 Ok(JValue::Null)
10938 }
10939 _ => Err(EvaluatorError::TypeError(format!(
10940 "Cannot compute modulo of {:?} and {:?}",
10941 left, right
10942 ))),
10943 }
10944 }
10945
10946 /// Get human-readable type name for error messages
10947 fn type_name(value: &JValue) -> &'static str {
10948 match value {
10949 JValue::Null => "null",
10950 JValue::Bool(_) => "boolean",
10951 JValue::Number(_) => "number",
10952 JValue::String(_) => "string",
10953 JValue::Array(_) => "array",
10954 JValue::Object(_) => "object",
10955 _ => "unknown",
10956 }
10957 }
10958
10959 /// Ordered comparison with null/type checking shared across <, <=, >, >=
10960 ///
10961 /// `compare_nums` receives (left_f64, right_f64) for numeric operands.
10962 /// `compare_strs` receives (left_str, right_str) for string operands.
10963 /// `op_symbol` is used in the T2009 error message (e.g. "<", ">=").
10964 fn ordered_compare(
10965 &self,
10966 left: &JValue,
10967 right: &JValue,
10968 left_is_explicit_null: bool,
10969 right_is_explicit_null: bool,
10970 op_symbol: &str,
10971 compare_nums: fn(f64, f64) -> bool,
10972 compare_strs: fn(&str, &str) -> bool,
10973 ) -> Result<JValue, EvaluatorError> {
10974 match (left, right) {
10975 (JValue::Number(a), JValue::Number(b)) => {
10976 Ok(JValue::Bool(compare_nums(*a, *b)))
10977 }
10978 (JValue::String(a), JValue::String(b)) => Ok(JValue::Bool(compare_strs(a, b))),
10979 // Both null/undefined -> return undefined
10980 (JValue::Null, JValue::Null) => Ok(JValue::Null),
10981 // Explicit null literal with any type (except null) -> T2010 error
10982 (JValue::Null, _) if left_is_explicit_null => {
10983 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10984 }
10985 (_, JValue::Null) if right_is_explicit_null => {
10986 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10987 }
10988 // Boolean with undefined -> T2010 error
10989 (JValue::Bool(_), JValue::Null) | (JValue::Null, JValue::Bool(_)) => {
10990 Err(EvaluatorError::EvaluationError("T2010: Type mismatch in comparison".to_string()))
10991 }
10992 // Number or String with undefined (not explicit null) -> undefined result
10993 (JValue::Number(_), JValue::Null) | (JValue::Null, JValue::Number(_)) |
10994 (JValue::String(_), JValue::Null) | (JValue::Null, JValue::String(_)) => {
10995 Ok(JValue::Null)
10996 }
10997 // String vs Number -> T2009
10998 (JValue::String(_), JValue::Number(_)) | (JValue::Number(_), JValue::String(_)) => {
10999 Err(EvaluatorError::EvaluationError(format!(
11000 "T2009: The expressions on either side of operator \"{}\" must be of the same data type",
11001 op_symbol
11002 )))
11003 }
11004 // Boolean comparisons -> T2010
11005 (JValue::Bool(_), _) | (_, JValue::Bool(_)) => {
11006 Err(EvaluatorError::EvaluationError(format!(
11007 "T2010: Cannot compare {} and {}",
11008 Self::type_name(left), Self::type_name(right)
11009 )))
11010 }
11011 // Other type mismatches
11012 _ => Err(EvaluatorError::EvaluationError(format!(
11013 "T2010: Cannot compare {} and {}",
11014 Self::type_name(left), Self::type_name(right)
11015 ))),
11016 }
11017 }
11018
11019 /// Less than comparison
11020 fn less_than(
11021 &self,
11022 left: &JValue,
11023 right: &JValue,
11024 left_is_explicit_null: bool,
11025 right_is_explicit_null: bool,
11026 ) -> Result<JValue, EvaluatorError> {
11027 self.ordered_compare(
11028 left,
11029 right,
11030 left_is_explicit_null,
11031 right_is_explicit_null,
11032 "<",
11033 |a, b| a < b,
11034 |a, b| a < b,
11035 )
11036 }
11037
11038 /// Less than or equal comparison
11039 fn less_than_or_equal(
11040 &self,
11041 left: &JValue,
11042 right: &JValue,
11043 left_is_explicit_null: bool,
11044 right_is_explicit_null: bool,
11045 ) -> Result<JValue, EvaluatorError> {
11046 self.ordered_compare(
11047 left,
11048 right,
11049 left_is_explicit_null,
11050 right_is_explicit_null,
11051 "<=",
11052 |a, b| a <= b,
11053 |a, b| a <= b,
11054 )
11055 }
11056
11057 /// Greater than comparison
11058 fn greater_than(
11059 &self,
11060 left: &JValue,
11061 right: &JValue,
11062 left_is_explicit_null: bool,
11063 right_is_explicit_null: bool,
11064 ) -> Result<JValue, EvaluatorError> {
11065 self.ordered_compare(
11066 left,
11067 right,
11068 left_is_explicit_null,
11069 right_is_explicit_null,
11070 ">",
11071 |a, b| a > b,
11072 |a, b| a > b,
11073 )
11074 }
11075
11076 /// Greater than or equal comparison
11077 fn greater_than_or_equal(
11078 &self,
11079 left: &JValue,
11080 right: &JValue,
11081 left_is_explicit_null: bool,
11082 right_is_explicit_null: bool,
11083 ) -> Result<JValue, EvaluatorError> {
11084 self.ordered_compare(
11085 left,
11086 right,
11087 left_is_explicit_null,
11088 right_is_explicit_null,
11089 ">=",
11090 |a, b| a >= b,
11091 |a, b| a >= b,
11092 )
11093 }
11094
11095 /// Convert a value to a string for concatenation
11096 fn value_to_concat_string(value: &JValue) -> Result<String, EvaluatorError> {
11097 match value {
11098 JValue::String(s) => Ok(s.to_string()),
11099 JValue::Null => Ok(String::new()),
11100 JValue::Number(_) | JValue::Bool(_) | JValue::Array(_) | JValue::Object(_) => {
11101 match crate::functions::string::string(value, None) {
11102 Ok(JValue::String(s)) => Ok(s.to_string()),
11103 Ok(JValue::Null) => Ok(String::new()),
11104 _ => Err(EvaluatorError::TypeError(
11105 "Cannot concatenate complex types".to_string(),
11106 )),
11107 }
11108 }
11109 _ => Ok(String::new()),
11110 }
11111 }
11112
11113 /// String concatenation
11114 fn concatenate(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11115 let left_str = Self::value_to_concat_string(left)?;
11116 let right_str = Self::value_to_concat_string(right)?;
11117 Ok(JValue::string(format!("{}{}", left_str, right_str)))
11118 }
11119
11120 /// Range operator (e.g., 1..5 produces [1,2,3,4,5])
11121 fn range(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11122 // Check left operand is a number or null
11123 let start_f64 = match left {
11124 JValue::Number(n) => Some(*n),
11125 JValue::Null | JValue::Undefined => None,
11126 _ => {
11127 return Err(EvaluatorError::EvaluationError(
11128 "T2003: Left operand of range operator must be a number".to_string(),
11129 ));
11130 }
11131 };
11132
11133 // Check left operand is an integer (if it's a number)
11134 if let Some(val) = start_f64 {
11135 if val.fract() != 0.0 {
11136 return Err(EvaluatorError::EvaluationError(
11137 "T2003: Left operand of range operator must be an integer".to_string(),
11138 ));
11139 }
11140 }
11141
11142 // Check right operand is a number or null
11143 let end_f64 = match right {
11144 JValue::Number(n) => Some(*n),
11145 JValue::Null | JValue::Undefined => None,
11146 _ => {
11147 return Err(EvaluatorError::EvaluationError(
11148 "T2004: Right operand of range operator must be a number".to_string(),
11149 ));
11150 }
11151 };
11152
11153 // Check right operand is an integer (if it's a number)
11154 if let Some(val) = end_f64 {
11155 if val.fract() != 0.0 {
11156 return Err(EvaluatorError::EvaluationError(
11157 "T2004: Right operand of range operator must be an integer".to_string(),
11158 ));
11159 }
11160 }
11161
11162 // If either operand is null, return empty array
11163 if start_f64.is_none() || end_f64.is_none() {
11164 return Ok(JValue::array(vec![]));
11165 }
11166
11167 let start = start_f64.unwrap() as i64;
11168 let end = end_f64.unwrap() as i64;
11169
11170 // Check range size limit (10 million elements max)
11171 let size = if start <= end {
11172 (end - start + 1) as usize
11173 } else {
11174 0
11175 };
11176 if size > 10_000_000 {
11177 return Err(EvaluatorError::EvaluationError(
11178 "D2014: Range operator results in too many elements (> 10,000,000)".to_string(),
11179 ));
11180 }
11181
11182 let mut result = Vec::with_capacity(size);
11183 if start <= end {
11184 for i in start..=end {
11185 result.push(JValue::Number(i as f64));
11186 }
11187 }
11188 // Note: if start > end, return empty array (not reversed)
11189 Ok(JValue::array(result))
11190 }
11191
11192 /// In operator (checks if left is in right array/object)
11193 /// Array indexing: array[index]
11194 fn array_index(&self, array: &JValue, index: &JValue) -> Result<JValue, EvaluatorError> {
11195 match (array, index) {
11196 (JValue::Array(arr), JValue::Number(n)) => {
11197 let idx = *n as i64;
11198 let len = arr.len() as i64;
11199
11200 // Handle negative indexing (offset from end)
11201 let actual_idx = if idx < 0 { len + idx } else { idx };
11202
11203 if actual_idx < 0 || actual_idx >= len {
11204 Ok(JValue::Undefined)
11205 } else {
11206 Ok(arr[actual_idx as usize].clone())
11207 }
11208 }
11209 _ => Err(EvaluatorError::TypeError(
11210 "Array indexing requires array and number".to_string(),
11211 )),
11212 }
11213 }
11214
11215 /// Array filtering: array[predicate]
11216 /// Evaluates the predicate for each item in the array and returns items where predicate is true
11217 fn array_filter(
11218 &mut self,
11219 _lhs_node: &AstNode,
11220 rhs_node: &AstNode,
11221 array: &JValue,
11222 _original_data: &JValue,
11223 ) -> Result<JValue, EvaluatorError> {
11224 match array {
11225 JValue::Array(arr) => {
11226 // Pre-allocate with estimated capacity (assume ~50% will match)
11227 let mut filtered = Vec::with_capacity(arr.len() / 2);
11228
11229 for item in arr.iter() {
11230 // Evaluate the predicate in the context of this array item
11231 // The item becomes the new "current context" ($)
11232 let predicate_result = self.evaluate_internal(rhs_node, item)?;
11233
11234 // Check if the predicate is truthy
11235 if self.is_truthy(&predicate_result) {
11236 filtered.push(item.clone());
11237 }
11238 }
11239
11240 Ok(JValue::array(filtered))
11241 }
11242 _ => Err(EvaluatorError::TypeError(
11243 "Array filtering requires an array".to_string(),
11244 )),
11245 }
11246 }
11247
11248 fn in_operator(&self, left: &JValue, right: &JValue) -> Result<JValue, EvaluatorError> {
11249 // If either side is undefined/null, return false (not an error)
11250 // This matches JavaScript behavior
11251 if left.is_null() || right.is_null() {
11252 return Ok(JValue::Bool(false));
11253 }
11254
11255 match right {
11256 JValue::Array(arr) => Ok(JValue::Bool(arr.iter().any(|v| self.equals(left, v)))),
11257 JValue::Object(obj) => {
11258 if let JValue::String(key) = left {
11259 Ok(JValue::Bool(obj.contains_key(&**key)))
11260 } else {
11261 Ok(JValue::Bool(false))
11262 }
11263 }
11264 // If right side is not an array or object (e.g., string, number),
11265 // wrap it in an array for comparison
11266 other => Ok(JValue::Bool(self.equals(left, other))),
11267 }
11268 }
11269
11270 /// Create a partially applied function from a function call with placeholder arguments
11271 /// This evaluates non-placeholder arguments and creates a new lambda that takes
11272 /// the placeholder positions as parameters.
11273 fn create_partial_application(
11274 &mut self,
11275 name: &str,
11276 args: &[AstNode],
11277 is_builtin: bool,
11278 data: &JValue,
11279 ) -> Result<JValue, EvaluatorError> {
11280 // First, look up the function to ensure it exists
11281 let is_lambda = self.context.lookup_lambda(name).is_some()
11282 || (self
11283 .context
11284 .lookup(name)
11285 .map(|v| matches!(v, JValue::Lambda { .. }))
11286 .unwrap_or(false));
11287
11288 // Built-in functions must be called with $ prefix for partial application
11289 // Without $, it's an error (T1007) suggesting the user forgot the $
11290 if !is_lambda && !is_builtin {
11291 // Check if it's a built-in function called without $
11292 if self.is_builtin_function(name) {
11293 return Err(EvaluatorError::EvaluationError(format!(
11294 "T1007: Attempted to partially apply a non-function. Did you mean ${}?",
11295 name
11296 )));
11297 }
11298 return Err(EvaluatorError::EvaluationError(
11299 "T1008: Attempted to partially apply a non-function".to_string(),
11300 ));
11301 }
11302
11303 // Evaluate non-placeholder arguments and track placeholder positions
11304 let mut bound_args: Vec<(usize, JValue)> = Vec::new();
11305 let mut placeholder_positions: Vec<usize> = Vec::new();
11306
11307 for (i, arg) in args.iter().enumerate() {
11308 if matches!(arg, AstNode::Placeholder) {
11309 placeholder_positions.push(i);
11310 } else {
11311 let value = self.evaluate_internal(arg, data)?;
11312 bound_args.push((i, value));
11313 }
11314 }
11315
11316 // Generate parameter names for each placeholder
11317 let param_names: Vec<String> = placeholder_positions
11318 .iter()
11319 .enumerate()
11320 .map(|(i, _)| format!("__p{}", i))
11321 .collect();
11322
11323 // Store the partial application info as a special lambda
11324 // When invoked, it will call the original function with bound + placeholder args
11325 let partial_id = format!(
11326 "__partial_{}_{}_{}",
11327 name,
11328 placeholder_positions.len(),
11329 bound_args.len()
11330 );
11331
11332 // Create a stored lambda that represents this partial application
11333 // The body is a marker that we'll interpret specially during invocation
11334 let stored_lambda = StoredLambda {
11335 params: param_names.clone(),
11336 body: AstNode::String(format!(
11337 "__partial_call:{}:{}:{}",
11338 name,
11339 is_builtin,
11340 args.len()
11341 )),
11342 compiled_body: None, // Partial application uses a special body marker
11343 signature: None,
11344 captured_env: {
11345 let mut env = self.capture_current_environment();
11346 // Store the bound arguments in the captured environment
11347 for (pos, value) in &bound_args {
11348 env.insert(format!("__bound_arg_{}", pos), value.clone());
11349 }
11350 // Store placeholder positions
11351 env.insert(
11352 "__placeholder_positions".to_string(),
11353 JValue::array(
11354 placeholder_positions
11355 .iter()
11356 .map(|p| JValue::Number(*p as f64))
11357 .collect::<Vec<_>>(),
11358 ),
11359 );
11360 // Store total argument count
11361 env.insert(
11362 "__total_args".to_string(),
11363 JValue::Number(args.len() as f64),
11364 );
11365 env
11366 },
11367 captured_data: Some(data.clone()),
11368 thunk: false,
11369 };
11370
11371 self.context.bind_lambda(partial_id.clone(), stored_lambda);
11372
11373 // Return a lambda object that can be invoked
11374 let lambda_obj = JValue::lambda(
11375 partial_id.as_str(),
11376 param_names,
11377 Some(name.to_string()),
11378 None::<String>,
11379 );
11380
11381 Ok(lambda_obj)
11382 }
11383}
11384
11385impl Default for Evaluator {
11386 fn default() -> Self {
11387 Self::new()
11388 }
11389}
11390
11391#[cfg(test)]
11392mod tests {
11393 use super::*;
11394 use crate::ast::{BinaryOp, UnaryOp};
11395
11396 // --- Task 7: tuple-wrapper output leak -----------------------------------
11397 //
11398 // `%`/`@`/`#` are implemented internally via a tuple-stream representation
11399 // (`create_tuple_stream`): each element gets wrapped as
11400 // `{"@": value, "__tuple__": true, ...bindings}`. Intermediate path steps
11401 // consume/re-wrap these, but the *final* evaluate() result can still carry
11402 // a lingering wrapper -- confirmed for real by dumping actual output before
11403 // this fix (see task-7-report.md for the raw before/after). These tests
11404 // pin both the bare top-level case (Task 5's brief `#` example) and the
11405 // object/array-construction-nested case (found while verifying the brief's
11406 // illustrative fix against real output -- a plain per-element Array-only
11407 // recursion does not reach into a constructed object's field values).
11408
11409 fn dataset5_for_tuple_tests() -> JValue {
11410 let s = include_str!("../tests/jsonata-js/test/test-suite/datasets/dataset5.json");
11411 serde_json::from_str::<serde_json::Value>(s).unwrap().into()
11412 }
11413
11414 fn assert_no_tuple_wrapper(value: &JValue) {
11415 match value {
11416 JValue::Object(obj) => {
11417 assert!(
11418 obj.get("__tuple__").is_none(),
11419 "tuple wrapper leaked into output: {:?}",
11420 value
11421 );
11422 for v in obj.values() {
11423 assert_no_tuple_wrapper(v);
11424 }
11425 }
11426 JValue::Array(arr) => {
11427 for item in arr.iter() {
11428 assert_no_tuple_wrapper(item);
11429 }
11430 }
11431 _ => {}
11432 }
11433 }
11434
11435 #[test]
11436 fn test_bare_index_bind_result_does_not_leak_tuple_wrapper() {
11437 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11438 let ast = crate::parser::parse("items#$i").unwrap();
11439 let mut evaluator = Evaluator::new();
11440 let result = evaluator.evaluate(&ast, &data).unwrap();
11441 assert_no_tuple_wrapper(&result);
11442 assert_eq!(
11443 result,
11444 JValue::array(vec![
11445 JValue::from(1i64),
11446 JValue::from(2i64),
11447 JValue::from(3i64)
11448 ])
11449 );
11450 }
11451
11452 #[test]
11453 fn test_percent_predicate_result_does_not_leak_tuple_wrapper() {
11454 // Confirmed by Task 6 to evaluate to the correct @-values but stay
11455 // wrapped: Account.Order.Product[%.OrderID='order104'].SKU
11456 let data = dataset5_for_tuple_tests();
11457 let ast = crate::parser::parse("Account.Order.Product[%.OrderID='order104'].SKU").unwrap();
11458 let mut evaluator = Evaluator::new();
11459 let result = evaluator.evaluate(&ast, &data).unwrap();
11460 assert_no_tuple_wrapper(&result);
11461 assert_eq!(
11462 result,
11463 JValue::array(vec![
11464 JValue::string("040657863"),
11465 JValue::string("0406654603"),
11466 ])
11467 );
11468 }
11469
11470 #[test]
11471 fn test_percent_step_over_tuple_stream_does_not_leak_tuple_wrapper() {
11472 // Confirmed by Task 6: Account.Order.Product.Price.%[%.OrderID='order103'].SKU
11473 let data = dataset5_for_tuple_tests();
11474 let ast = crate::parser::parse("Account.Order.Product.Price.%[%.OrderID='order103'].SKU")
11475 .unwrap();
11476 let mut evaluator = Evaluator::new();
11477 let result = evaluator.evaluate(&ast, &data).unwrap();
11478 assert_no_tuple_wrapper(&result);
11479 assert_eq!(
11480 result,
11481 JValue::array(vec![
11482 JValue::string("0406654608"),
11483 JValue::string("0406634348"),
11484 ])
11485 );
11486 }
11487
11488 #[test]
11489 fn test_tuple_wrapper_does_not_leak_when_nested_in_object_construction() {
11490 // A tuple-producing expression nested inside a constructed object's field
11491 // value: the top-level result is a plain (non-tuple) Object, so a naive
11492 // "unwrap only if the whole value is a tuple wrapper" check would miss
11493 // this -- must recurse into field values too.
11494 let data = dataset5_for_tuple_tests();
11495 let ast =
11496 crate::parser::parse(r#"{ "skus": Account.Order.Product[%.OrderID='order104'].SKU }"#)
11497 .unwrap();
11498 let mut evaluator = Evaluator::new();
11499 let result = evaluator.evaluate(&ast, &data).unwrap();
11500 assert_no_tuple_wrapper(&result);
11501 assert_eq!(
11502 result,
11503 JValue::from(serde_json::json!({
11504 "skus": ["040657863", "0406654603"]
11505 }))
11506 );
11507 }
11508
11509 #[test]
11510 fn test_tuple_wrapper_does_not_leak_when_nested_in_array_construction() {
11511 let data: JValue = serde_json::json!({"items": [1, 2, 3]}).into();
11512 let ast = crate::parser::parse("[items#$i]").unwrap();
11513 let mut evaluator = Evaluator::new();
11514 let result = evaluator.evaluate(&ast, &data).unwrap();
11515 assert_no_tuple_wrapper(&result);
11516 }
11517
11518 #[test]
11519 fn test_evaluate_literals() {
11520 let mut evaluator = Evaluator::new();
11521 let data = JValue::Null;
11522
11523 // String literal
11524 let result = evaluator
11525 .evaluate(&AstNode::string("hello"), &data)
11526 .unwrap();
11527 assert_eq!(result, JValue::string("hello"));
11528
11529 // Number literal
11530 let result = evaluator.evaluate(&AstNode::number(42.0), &data).unwrap();
11531 assert_eq!(result, JValue::from(42i64));
11532
11533 // Boolean literal
11534 let result = evaluator.evaluate(&AstNode::boolean(true), &data).unwrap();
11535 assert_eq!(result, JValue::Bool(true));
11536
11537 // Null literal
11538 let result = evaluator.evaluate(&AstNode::null(), &data).unwrap();
11539 assert_eq!(result, JValue::Null);
11540 }
11541
11542 #[test]
11543 fn test_evaluate_variables() {
11544 let mut evaluator = Evaluator::new();
11545 let data = JValue::Null;
11546
11547 // Bind a variable
11548 evaluator
11549 .context
11550 .bind("x".to_string(), JValue::from(100i64));
11551
11552 // Look up the variable
11553 let result = evaluator.evaluate(&AstNode::variable("x"), &data).unwrap();
11554 assert_eq!(result, JValue::from(100i64));
11555
11556 // Undefined variable returns null (undefined in JSONata semantics)
11557 let result = evaluator
11558 .evaluate(&AstNode::variable("undefined"), &data)
11559 .unwrap();
11560 assert_eq!(result, JValue::Null);
11561 }
11562
11563 #[test]
11564 fn test_evaluate_path() {
11565 let mut evaluator = Evaluator::new();
11566 let data = JValue::from(serde_json::json!({
11567 "foo": {
11568 "bar": {
11569 "baz": 42
11570 }
11571 }
11572 }));
11573 // Simple path
11574 let path = AstNode::Path {
11575 steps: vec![PathStep::new(AstNode::Name("foo".to_string()))],
11576 };
11577 let result = evaluator.evaluate(&path, &data).unwrap();
11578 assert_eq!(
11579 result,
11580 JValue::from(serde_json::json!({"bar": {"baz": 42}}))
11581 );
11582
11583 // Nested path
11584 let path = AstNode::Path {
11585 steps: vec![
11586 PathStep::new(AstNode::Name("foo".to_string())),
11587 PathStep::new(AstNode::Name("bar".to_string())),
11588 PathStep::new(AstNode::Name("baz".to_string())),
11589 ],
11590 };
11591 let result = evaluator.evaluate(&path, &data).unwrap();
11592 assert_eq!(result, JValue::from(42i64));
11593
11594 // Missing path returns undefined (not null - see issue #32)
11595 let path = AstNode::Path {
11596 steps: vec![PathStep::new(AstNode::Name("missing".to_string()))],
11597 };
11598 let result = evaluator.evaluate(&path, &data).unwrap();
11599 assert_eq!(result, JValue::Undefined);
11600 }
11601
11602 #[test]
11603 fn test_arithmetic_operations() {
11604 let mut evaluator = Evaluator::new();
11605 let data = JValue::Null;
11606
11607 // Addition
11608 let expr = AstNode::Binary {
11609 op: BinaryOp::Add,
11610 lhs: Box::new(AstNode::number(10.0)),
11611 rhs: Box::new(AstNode::number(5.0)),
11612 };
11613 let result = evaluator.evaluate(&expr, &data).unwrap();
11614 assert_eq!(result, JValue::Number(15.0));
11615
11616 // Subtraction
11617 let expr = AstNode::Binary {
11618 op: BinaryOp::Subtract,
11619 lhs: Box::new(AstNode::number(10.0)),
11620 rhs: Box::new(AstNode::number(5.0)),
11621 };
11622 let result = evaluator.evaluate(&expr, &data).unwrap();
11623 assert_eq!(result, JValue::Number(5.0));
11624
11625 // Multiplication
11626 let expr = AstNode::Binary {
11627 op: BinaryOp::Multiply,
11628 lhs: Box::new(AstNode::number(10.0)),
11629 rhs: Box::new(AstNode::number(5.0)),
11630 };
11631 let result = evaluator.evaluate(&expr, &data).unwrap();
11632 assert_eq!(result, JValue::Number(50.0));
11633
11634 // Division
11635 let expr = AstNode::Binary {
11636 op: BinaryOp::Divide,
11637 lhs: Box::new(AstNode::number(10.0)),
11638 rhs: Box::new(AstNode::number(5.0)),
11639 };
11640 let result = evaluator.evaluate(&expr, &data).unwrap();
11641 assert_eq!(result, JValue::Number(2.0));
11642
11643 // Modulo
11644 let expr = AstNode::Binary {
11645 op: BinaryOp::Modulo,
11646 lhs: Box::new(AstNode::number(10.0)),
11647 rhs: Box::new(AstNode::number(3.0)),
11648 };
11649 let result = evaluator.evaluate(&expr, &data).unwrap();
11650 assert_eq!(result, JValue::Number(1.0));
11651 }
11652
11653 #[test]
11654 fn test_division_by_zero() {
11655 let mut evaluator = Evaluator::new();
11656 let data = JValue::Null;
11657
11658 let expr = AstNode::Binary {
11659 op: BinaryOp::Divide,
11660 lhs: Box::new(AstNode::number(10.0)),
11661 rhs: Box::new(AstNode::number(0.0)),
11662 };
11663 let result = evaluator.evaluate(&expr, &data);
11664 assert!(result.is_err());
11665 }
11666
11667 #[test]
11668 fn test_comparison_operations() {
11669 let mut evaluator = Evaluator::new();
11670 let data = JValue::Null;
11671
11672 // Equal
11673 let expr = AstNode::Binary {
11674 op: BinaryOp::Equal,
11675 lhs: Box::new(AstNode::number(5.0)),
11676 rhs: Box::new(AstNode::number(5.0)),
11677 };
11678 assert_eq!(
11679 evaluator.evaluate(&expr, &data).unwrap(),
11680 JValue::Bool(true)
11681 );
11682
11683 // Not equal
11684 let expr = AstNode::Binary {
11685 op: BinaryOp::NotEqual,
11686 lhs: Box::new(AstNode::number(5.0)),
11687 rhs: Box::new(AstNode::number(3.0)),
11688 };
11689 assert_eq!(
11690 evaluator.evaluate(&expr, &data).unwrap(),
11691 JValue::Bool(true)
11692 );
11693
11694 // Less than
11695 let expr = AstNode::Binary {
11696 op: BinaryOp::LessThan,
11697 lhs: Box::new(AstNode::number(3.0)),
11698 rhs: Box::new(AstNode::number(5.0)),
11699 };
11700 assert_eq!(
11701 evaluator.evaluate(&expr, &data).unwrap(),
11702 JValue::Bool(true)
11703 );
11704
11705 // Greater than
11706 let expr = AstNode::Binary {
11707 op: BinaryOp::GreaterThan,
11708 lhs: Box::new(AstNode::number(5.0)),
11709 rhs: Box::new(AstNode::number(3.0)),
11710 };
11711 assert_eq!(
11712 evaluator.evaluate(&expr, &data).unwrap(),
11713 JValue::Bool(true)
11714 );
11715 }
11716
11717 #[test]
11718 fn test_logical_operations() {
11719 let mut evaluator = Evaluator::new();
11720 let data = JValue::Null;
11721
11722 // And - both true
11723 let expr = AstNode::Binary {
11724 op: BinaryOp::And,
11725 lhs: Box::new(AstNode::boolean(true)),
11726 rhs: Box::new(AstNode::boolean(true)),
11727 };
11728 assert_eq!(
11729 evaluator.evaluate(&expr, &data).unwrap(),
11730 JValue::Bool(true)
11731 );
11732
11733 // And - first false
11734 let expr = AstNode::Binary {
11735 op: BinaryOp::And,
11736 lhs: Box::new(AstNode::boolean(false)),
11737 rhs: Box::new(AstNode::boolean(true)),
11738 };
11739 assert_eq!(
11740 evaluator.evaluate(&expr, &data).unwrap(),
11741 JValue::Bool(false)
11742 );
11743
11744 // Or - first true
11745 let expr = AstNode::Binary {
11746 op: BinaryOp::Or,
11747 lhs: Box::new(AstNode::boolean(true)),
11748 rhs: Box::new(AstNode::boolean(false)),
11749 };
11750 assert_eq!(
11751 evaluator.evaluate(&expr, &data).unwrap(),
11752 JValue::Bool(true)
11753 );
11754
11755 // Or - both false
11756 let expr = AstNode::Binary {
11757 op: BinaryOp::Or,
11758 lhs: Box::new(AstNode::boolean(false)),
11759 rhs: Box::new(AstNode::boolean(false)),
11760 };
11761 assert_eq!(
11762 evaluator.evaluate(&expr, &data).unwrap(),
11763 JValue::Bool(false)
11764 );
11765 }
11766
11767 #[test]
11768 fn test_string_concatenation() {
11769 let mut evaluator = Evaluator::new();
11770 let data = JValue::Null;
11771
11772 let expr = AstNode::Binary {
11773 op: BinaryOp::Concatenate,
11774 lhs: Box::new(AstNode::string("Hello")),
11775 rhs: Box::new(AstNode::string(" World")),
11776 };
11777 let result = evaluator.evaluate(&expr, &data).unwrap();
11778 assert_eq!(result, JValue::string("Hello World"));
11779 }
11780
11781 #[test]
11782 fn test_range_operator() {
11783 let mut evaluator = Evaluator::new();
11784 let data = JValue::Null;
11785
11786 // Forward range
11787 let expr = AstNode::Binary {
11788 op: BinaryOp::Range,
11789 lhs: Box::new(AstNode::number(1.0)),
11790 rhs: Box::new(AstNode::number(5.0)),
11791 };
11792 let result = evaluator.evaluate(&expr, &data).unwrap();
11793 assert_eq!(
11794 result,
11795 JValue::array(vec![
11796 JValue::Number(1.0),
11797 JValue::Number(2.0),
11798 JValue::Number(3.0),
11799 JValue::Number(4.0),
11800 JValue::Number(5.0)
11801 ])
11802 );
11803
11804 // Backward range (start > end) returns empty array
11805 let expr = AstNode::Binary {
11806 op: BinaryOp::Range,
11807 lhs: Box::new(AstNode::number(5.0)),
11808 rhs: Box::new(AstNode::number(1.0)),
11809 };
11810 let result = evaluator.evaluate(&expr, &data).unwrap();
11811 assert_eq!(result, JValue::array(vec![]));
11812 }
11813
11814 #[test]
11815 fn test_in_operator() {
11816 let mut evaluator = Evaluator::new();
11817 let data = JValue::Null;
11818
11819 // In array
11820 let expr = AstNode::Binary {
11821 op: BinaryOp::In,
11822 lhs: Box::new(AstNode::number(3.0)),
11823 rhs: Box::new(AstNode::Array(vec![
11824 AstNode::number(1.0),
11825 AstNode::number(2.0),
11826 AstNode::number(3.0),
11827 ])),
11828 };
11829 let result = evaluator.evaluate(&expr, &data).unwrap();
11830 assert_eq!(result, JValue::Bool(true));
11831
11832 // Not in array
11833 let expr = AstNode::Binary {
11834 op: BinaryOp::In,
11835 lhs: Box::new(AstNode::number(5.0)),
11836 rhs: Box::new(AstNode::Array(vec![
11837 AstNode::number(1.0),
11838 AstNode::number(2.0),
11839 AstNode::number(3.0),
11840 ])),
11841 };
11842 let result = evaluator.evaluate(&expr, &data).unwrap();
11843 assert_eq!(result, JValue::Bool(false));
11844 }
11845
11846 #[test]
11847 fn test_unary_operations() {
11848 let mut evaluator = Evaluator::new();
11849 let data = JValue::Null;
11850
11851 // Negation
11852 let expr = AstNode::Unary {
11853 op: UnaryOp::Negate,
11854 operand: Box::new(AstNode::number(5.0)),
11855 };
11856 let result = evaluator.evaluate(&expr, &data).unwrap();
11857 assert_eq!(result, JValue::Number(-5.0));
11858
11859 // Not
11860 let expr = AstNode::Unary {
11861 op: UnaryOp::Not,
11862 operand: Box::new(AstNode::boolean(true)),
11863 };
11864 let result = evaluator.evaluate(&expr, &data).unwrap();
11865 assert_eq!(result, JValue::Bool(false));
11866 }
11867
11868 #[test]
11869 fn test_array_construction() {
11870 let mut evaluator = Evaluator::new();
11871 let data = JValue::Null;
11872
11873 let expr = AstNode::Array(vec![
11874 AstNode::number(1.0),
11875 AstNode::number(2.0),
11876 AstNode::number(3.0),
11877 ]);
11878 let result = evaluator.evaluate(&expr, &data).unwrap();
11879 // Whole number literals are preserved as integers
11880 assert_eq!(result, JValue::from(serde_json::json!([1, 2, 3])));
11881 }
11882
11883 #[test]
11884 fn test_object_construction() {
11885 let mut evaluator = Evaluator::new();
11886 let data = JValue::Null;
11887
11888 let expr = AstNode::Object(vec![
11889 (AstNode::string("name"), AstNode::string("Alice")),
11890 (AstNode::string("age"), AstNode::number(30.0)),
11891 ]);
11892 let result = evaluator.evaluate(&expr, &data).unwrap();
11893 // Whole number literals are preserved as integers
11894 let mut expected = IndexMap::new();
11895 expected.insert("name".to_string(), JValue::string("Alice"));
11896 expected.insert("age".to_string(), JValue::Number(30.0));
11897 assert_eq!(result, JValue::object(expected));
11898 }
11899
11900 #[test]
11901 fn test_conditional() {
11902 let mut evaluator = Evaluator::new();
11903 let data = JValue::Null;
11904
11905 // True condition
11906 let expr = AstNode::Conditional {
11907 condition: Box::new(AstNode::boolean(true)),
11908 then_branch: Box::new(AstNode::string("yes")),
11909 else_branch: Some(Box::new(AstNode::string("no"))),
11910 };
11911 let result = evaluator.evaluate(&expr, &data).unwrap();
11912 assert_eq!(result, JValue::string("yes"));
11913
11914 // False condition
11915 let expr = AstNode::Conditional {
11916 condition: Box::new(AstNode::boolean(false)),
11917 then_branch: Box::new(AstNode::string("yes")),
11918 else_branch: Some(Box::new(AstNode::string("no"))),
11919 };
11920 let result = evaluator.evaluate(&expr, &data).unwrap();
11921 assert_eq!(result, JValue::string("no"));
11922
11923 // No else branch returns undefined (not null)
11924 let expr = AstNode::Conditional {
11925 condition: Box::new(AstNode::boolean(false)),
11926 then_branch: Box::new(AstNode::string("yes")),
11927 else_branch: None,
11928 };
11929 let result = evaluator.evaluate(&expr, &data).unwrap();
11930 assert_eq!(result, JValue::Undefined);
11931 }
11932
11933 #[test]
11934 fn test_block_expression() {
11935 let mut evaluator = Evaluator::new();
11936 let data = JValue::Null;
11937
11938 let expr = AstNode::Block(vec![
11939 AstNode::number(1.0),
11940 AstNode::number(2.0),
11941 AstNode::number(3.0),
11942 ]);
11943 let result = evaluator.evaluate(&expr, &data).unwrap();
11944 // Block returns the last expression; whole numbers are preserved as integers
11945 assert_eq!(result, JValue::from(3i64));
11946 }
11947
11948 #[test]
11949 fn test_function_calls() {
11950 let mut evaluator = Evaluator::new();
11951 let data = JValue::Null;
11952
11953 // uppercase function
11954 let expr = AstNode::Function {
11955 name: "uppercase".to_string(),
11956 args: vec![AstNode::string("hello")],
11957 is_builtin: true,
11958 };
11959 let result = evaluator.evaluate(&expr, &data).unwrap();
11960 assert_eq!(result, JValue::string("HELLO"));
11961
11962 // lowercase function
11963 let expr = AstNode::Function {
11964 name: "lowercase".to_string(),
11965 args: vec![AstNode::string("HELLO")],
11966 is_builtin: true,
11967 };
11968 let result = evaluator.evaluate(&expr, &data).unwrap();
11969 assert_eq!(result, JValue::string("hello"));
11970
11971 // length function
11972 let expr = AstNode::Function {
11973 name: "length".to_string(),
11974 args: vec![AstNode::string("hello")],
11975 is_builtin: true,
11976 };
11977 let result = evaluator.evaluate(&expr, &data).unwrap();
11978 assert_eq!(result, JValue::from(5i64));
11979
11980 // sum function
11981 let expr = AstNode::Function {
11982 name: "sum".to_string(),
11983 args: vec![AstNode::Array(vec![
11984 AstNode::number(1.0),
11985 AstNode::number(2.0),
11986 AstNode::number(3.0),
11987 ])],
11988 is_builtin: true,
11989 };
11990 let result = evaluator.evaluate(&expr, &data).unwrap();
11991 assert_eq!(result, JValue::Number(6.0));
11992
11993 // count function
11994 let expr = AstNode::Function {
11995 name: "count".to_string(),
11996 args: vec![AstNode::Array(vec![
11997 AstNode::number(1.0),
11998 AstNode::number(2.0),
11999 AstNode::number(3.0),
12000 ])],
12001 is_builtin: true,
12002 };
12003 let result = evaluator.evaluate(&expr, &data).unwrap();
12004 assert_eq!(result, JValue::from(3i64));
12005 }
12006
12007 #[test]
12008 fn test_complex_nested_data() {
12009 let mut evaluator = Evaluator::new();
12010 let data = JValue::from(serde_json::json!({
12011 "users": [
12012 {"name": "Alice", "age": 30},
12013 {"name": "Bob", "age": 25},
12014 {"name": "Charlie", "age": 35}
12015 ],
12016 "metadata": {
12017 "total": 3,
12018 "version": "1.0"
12019 }
12020 }));
12021 // Access nested field
12022 let path = AstNode::Path {
12023 steps: vec![
12024 PathStep::new(AstNode::Name("metadata".to_string())),
12025 PathStep::new(AstNode::Name("version".to_string())),
12026 ],
12027 };
12028 let result = evaluator.evaluate(&path, &data).unwrap();
12029 assert_eq!(result, JValue::string("1.0"));
12030 }
12031
12032 #[test]
12033 fn test_error_handling() {
12034 let mut evaluator = Evaluator::new();
12035 let data = JValue::Null;
12036
12037 // Type error: adding string and number
12038 let expr = AstNode::Binary {
12039 op: BinaryOp::Add,
12040 lhs: Box::new(AstNode::string("hello")),
12041 rhs: Box::new(AstNode::number(5.0)),
12042 };
12043 let result = evaluator.evaluate(&expr, &data);
12044 assert!(result.is_err());
12045
12046 // Reference error: undefined function
12047 let expr = AstNode::Function {
12048 name: "undefined_function".to_string(),
12049 args: vec![],
12050 is_builtin: false,
12051 };
12052 let result = evaluator.evaluate(&expr, &data);
12053 assert!(result.is_err());
12054 }
12055
12056 #[test]
12057 fn test_truthiness() {
12058 let evaluator = Evaluator::new();
12059
12060 assert!(!evaluator.is_truthy(&JValue::Null));
12061 assert!(!evaluator.is_truthy(&JValue::Bool(false)));
12062 assert!(evaluator.is_truthy(&JValue::Bool(true)));
12063 assert!(!evaluator.is_truthy(&JValue::from(0i64)));
12064 assert!(evaluator.is_truthy(&JValue::from(1i64)));
12065 assert!(!evaluator.is_truthy(&JValue::string("")));
12066 assert!(evaluator.is_truthy(&JValue::string("hello")));
12067 assert!(!evaluator.is_truthy(&JValue::array(vec![])));
12068 assert!(evaluator.is_truthy(&JValue::from(serde_json::json!([1, 2, 3]))));
12069 }
12070
12071 #[test]
12072 fn test_integration_with_parser() {
12073 use crate::parser::parse;
12074
12075 let mut evaluator = Evaluator::new();
12076 let data = JValue::from(serde_json::json!({
12077 "price": 10,
12078 "quantity": 5
12079 }));
12080 // Test simple path
12081 let ast = parse("price").unwrap();
12082 let result = evaluator.evaluate(&ast, &data).unwrap();
12083 assert_eq!(result, JValue::from(10i64));
12084
12085 // Test arithmetic
12086 let ast = parse("price * quantity").unwrap();
12087 let result = evaluator.evaluate(&ast, &data).unwrap();
12088 // Note: Arithmetic operations produce f64 results in JSON
12089 assert_eq!(result, JValue::Number(50.0));
12090
12091 // Test comparison
12092 let ast = parse("price > 5").unwrap();
12093 let result = evaluator.evaluate(&ast, &data).unwrap();
12094 assert_eq!(result, JValue::Bool(true));
12095 }
12096
12097 #[test]
12098 fn test_evaluate_dollar_function_uppercase() {
12099 use crate::parser::parse;
12100
12101 let mut evaluator = Evaluator::new();
12102 let ast = parse(r#"$uppercase("hello")"#).unwrap();
12103 let empty = JValue::object(IndexMap::new());
12104 let result = evaluator.evaluate(&ast, &empty).unwrap();
12105 assert_eq!(result, JValue::string("HELLO"));
12106 }
12107
12108 #[test]
12109 fn test_evaluate_dollar_function_sum() {
12110 use crate::parser::parse;
12111
12112 let mut evaluator = Evaluator::new();
12113 let ast = parse("$sum([1, 2, 3, 4, 5])").unwrap();
12114 let empty = JValue::object(IndexMap::new());
12115 let result = evaluator.evaluate(&ast, &empty).unwrap();
12116 assert_eq!(result, JValue::Number(15.0));
12117 }
12118
12119 #[test]
12120 fn test_evaluate_nested_dollar_functions() {
12121 use crate::parser::parse;
12122
12123 let mut evaluator = Evaluator::new();
12124 let ast = parse(r#"$length($lowercase("HELLO"))"#).unwrap();
12125 let empty = JValue::object(IndexMap::new());
12126 let result = evaluator.evaluate(&ast, &empty).unwrap();
12127 // length() returns an integer, not a float
12128 assert_eq!(result, JValue::Number(5.0));
12129 }
12130
12131 #[test]
12132 fn test_array_mapping() {
12133 use crate::parser::parse;
12134
12135 let mut evaluator = Evaluator::new();
12136 let data: JValue = serde_json::from_str(
12137 r#"{
12138 "products": [
12139 {"id": 1, "name": "Laptop", "price": 999.99},
12140 {"id": 2, "name": "Mouse", "price": 29.99},
12141 {"id": 3, "name": "Keyboard", "price": 79.99}
12142 ]
12143 }"#,
12144 )
12145 .map(|v: serde_json::Value| JValue::from(v))
12146 .unwrap();
12147
12148 // Test mapping over array to extract field
12149 let ast = parse("products.name").unwrap();
12150 let result = evaluator.evaluate(&ast, &data).unwrap();
12151 assert_eq!(
12152 result,
12153 JValue::array(vec![
12154 JValue::string("Laptop"),
12155 JValue::string("Mouse"),
12156 JValue::string("Keyboard")
12157 ])
12158 );
12159
12160 // Test mapping over array to extract prices
12161 let ast = parse("products.price").unwrap();
12162 let result = evaluator.evaluate(&ast, &data).unwrap();
12163 assert_eq!(
12164 result,
12165 JValue::array(vec![
12166 JValue::Number(999.99),
12167 JValue::Number(29.99),
12168 JValue::Number(79.99)
12169 ])
12170 );
12171
12172 // Test with $sum function on mapped array
12173 let ast = parse("$sum(products.price)").unwrap();
12174 let result = evaluator.evaluate(&ast, &data).unwrap();
12175 assert_eq!(result, JValue::Number(1109.97));
12176 }
12177
12178 #[test]
12179 fn test_empty_brackets() {
12180 use crate::parser::parse;
12181
12182 let mut evaluator = Evaluator::new();
12183
12184 // Test empty brackets on simple value - should wrap in array
12185 let data: JValue = JValue::from(serde_json::json!({"foo": "bar"}));
12186 let ast = parse("foo[]").unwrap();
12187 let result = evaluator.evaluate(&ast, &data).unwrap();
12188 assert_eq!(
12189 result,
12190 JValue::array(vec![JValue::string("bar")]),
12191 "Empty brackets should wrap value in array"
12192 );
12193
12194 // Test empty brackets on array - should return array as-is
12195 let data2: JValue = JValue::from(serde_json::json!({"arr": [1, 2, 3]}));
12196 let ast2 = parse("arr[]").unwrap();
12197 let result2 = evaluator.evaluate(&ast2, &data2).unwrap();
12198 assert_eq!(
12199 result2,
12200 JValue::array(vec![
12201 JValue::Number(1.0),
12202 JValue::Number(2.0),
12203 JValue::Number(3.0)
12204 ]),
12205 "Empty brackets should preserve array"
12206 );
12207 }
12208
12209 // ---- Tuple-stream runtime: %/@/# binding operators (Task 5) ----
12210 // Expected values below are ground-truthed against jsonata-js 2.x.
12211
12212 #[test]
12213 fn test_index_bind_makes_variable_available_in_next_step() {
12214 // `#$o` binds each Order's position; `$o` must resolve in the later step.
12215 let data: JValue = serde_json::json!({
12216 "Account": {
12217 "Order": [
12218 {"OrderID": "o1", "Product": [{"Name": "Hat"}]},
12219 {"OrderID": "o2", "Product": [{"Name": "Cap"}, {"Name": "Sock"}]}
12220 ]
12221 }
12222 })
12223 .into();
12224 let ast =
12225 crate::parser::parse("Account.Order#$o.Product.{ 'name': Name, 'idx': $o }").unwrap();
12226 let mut evaluator = Evaluator::new();
12227 let result = evaluator.evaluate(&ast, &data).unwrap();
12228 assert_eq!(
12229 result,
12230 serde_json::json!([
12231 {"name": "Hat", "idx": 0},
12232 {"name": "Cap", "idx": 1},
12233 {"name": "Sock", "idx": 1}
12234 ])
12235 .into()
12236 );
12237 }
12238
12239 #[test]
12240 fn test_index_bind_with_predicate_stage() {
12241 // Mirrors reference joins/index[13]: index binding, then a predicate on
12242 // the next step, carrying the index binding through.
12243 let data: JValue = serde_json::json!({
12244 "Account": {
12245 "Order": [
12246 {"Product": [{"ProductID": 1, "Name": "A"}, {"ProductID": 9, "Name": "B"}]},
12247 {"Product": [{"ProductID": 9, "Name": "C"}]}
12248 ]
12249 }
12250 })
12251 .into();
12252 let ast =
12253 crate::parser::parse("Account.Order#$o.Product[ProductID=9].{ 'n': Name, 'idx': $o }")
12254 .unwrap();
12255 let mut evaluator = Evaluator::new();
12256 let result = evaluator.evaluate(&ast, &data).unwrap();
12257 assert_eq!(
12258 result,
12259 serde_json::json!([
12260 {"n": "B", "idx": 0},
12261 {"n": "C", "idx": 1}
12262 ])
12263 .into()
12264 );
12265 }
12266
12267 #[test]
12268 fn test_focus_bind_makes_variable_available_in_next_step() {
12269 // NOTE: `Account.Order@$o.Product` is `undefined` in jsonata-js (focus
12270 // does NOT advance the context `@`); the variable itself is what carries
12271 // forward. This asserts the real jsonata-js behaviour.
12272 let data: JValue = serde_json::json!({
12273 "Account": {
12274 "Order": [
12275 {"OrderID": "o1"},
12276 {"OrderID": "o2"}
12277 ]
12278 }
12279 })
12280 .into();
12281 let ast = crate::parser::parse("Account.Order@$o.$o.OrderID").unwrap();
12282 let mut evaluator = Evaluator::new();
12283 let result = evaluator.evaluate(&ast, &data).unwrap();
12284 assert_eq!(result, serde_json::json!(["o1", "o2"]).into());
12285 }
12286
12287 #[test]
12288 fn test_parent_reference_resolves_to_enclosing_step_value() {
12289 let data: JValue = serde_json::json!({
12290 "Account": {
12291 "Order": [
12292 {"OrderID": "o1", "Product": [{"Name": "Hat"}]}
12293 ]
12294 }
12295 })
12296 .into();
12297 let ast =
12298 crate::parser::parse("Account.Order.Product.{ 'name': Name, 'order': %.OrderID }")
12299 .unwrap();
12300 let mut evaluator = Evaluator::new();
12301 let result = evaluator.evaluate(&ast, &data).unwrap();
12302 assert_eq!(
12303 result,
12304 serde_json::json!([{"name": "Hat", "order": "o1"}]).into()
12305 );
12306 }
12307
12308 // Regression tests for a bug where create_tuple_stream/evaluate_sort bound
12309 // a tuple-carried `$name`/`!label` key straight into the top scope and then
12310 // UNCONDITIONALLY unbound it afterward, deleting (rather than restoring) a
12311 // same-named outer `:=` binding that happened to be live in that scope
12312 // frame. Expected values below are verified against jsonata-js (2.2.1
12313 // reference, `tests/jsonata-js`).
12314
12315 #[test]
12316 fn test_chained_focus_bind_does_not_clobber_outer_variable() {
12317 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12318 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b@$y.c; $x)"#).unwrap();
12319 let mut evaluator = Evaluator::new();
12320 let result = evaluator.evaluate(&ast, &data).unwrap();
12321 assert_eq!(result, serde_json::json!("OUT").into());
12322 }
12323
12324 #[test]
12325 fn test_chained_index_bind_does_not_clobber_outer_variable() {
12326 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12327 let ast = crate::parser::parse(r#"($x := "OUT"; a#$x.b#$y.c; $x)"#).unwrap();
12328 let mut evaluator = Evaluator::new();
12329 let result = evaluator.evaluate(&ast, &data).unwrap();
12330 assert_eq!(result, serde_json::json!("OUT").into());
12331 }
12332
12333 #[test]
12334 fn test_mixed_focus_and_index_bind_does_not_clobber_outer_variable() {
12335 let data: JValue = serde_json::json!({"a": {"b": {"c": 1}}}).into();
12336 let ast = crate::parser::parse(r#"($x := "OUT"; a@$x.b#$y.c; $x)"#).unwrap();
12337 let mut evaluator = Evaluator::new();
12338 let result = evaluator.evaluate(&ast, &data).unwrap();
12339 assert_eq!(result, serde_json::json!("OUT").into());
12340 }
12341
12342 #[test]
12343 fn test_sort_term_tuple_binding_does_not_clobber_outer_variable() {
12344 let data: JValue = serde_json::json!({"items": [{"v": 3}, {"v": 1}, {"v": 2}]}).into();
12345 let ast = crate::parser::parse(r#"($x := "OUT"; items@$x.v^(%.v); $x)"#).unwrap();
12346 let mut evaluator = Evaluator::new();
12347 let result = evaluator.evaluate(&ast, &data).unwrap();
12348 assert_eq!(result, serde_json::json!("OUT").into());
12349 }
12350}