Skip to main content

pmcp_code_mode/
eval.rs

1//! Shared expression evaluation logic for Code Mode.
2//!
3//! This module provides core evaluation functions that are used by both
4//! synchronous (`PlanExecutor`) and asynchronous (`AsyncScope`) executors.
5//! By centralizing this logic, we avoid duplication and ensure consistent
6//! behavior across execution modes.
7//!
8//! ## Design
9//!
10//! The evaluation functions are generic over a `VariableProvider` trait,
11//! allowing them to work with different variable storage mechanisms:
12//!
13//! ```ignore
14//! // Sync executor uses HashMap directly
15//! let value = evaluate_expr(&expr, &sync_vars, &local_scope)?;
16//!
17//! // Async executor uses the same functions
18//! let value = evaluate_expr(&expr, &async_vars, &local_scope)?;
19//! ```
20
21use crate::types::ExecutionError;
22use serde_json::Value as JsonValue;
23use std::collections::HashMap;
24
25use crate::executor::{
26    ArrayMethodCall, BinaryOperator, BuiltinFunction, NumberMethodCall, ObjectField, UnaryOperator,
27    ValueExpr,
28};
29
30/// Trait for providing variable values during evaluation.
31///
32/// This abstraction allows the evaluation functions to work with
33/// different variable storage mechanisms (HashMap, async state, etc.).
34pub trait VariableProvider {
35    /// Get a variable value by name.
36    fn get_variable(&self, name: &str) -> Option<&JsonValue>;
37}
38
39/// Simple HashMap-based variable provider.
40impl VariableProvider for HashMap<String, JsonValue> {
41    fn get_variable(&self, name: &str) -> Option<&JsonValue> {
42        self.get(name)
43    }
44}
45
46/// Evaluate a `ValueExpr` with access to global and local variables.
47///
48/// This is the core evaluation function used by both sync and async executors.
49/// It recursively evaluates expressions, properly handling scope for nested
50/// callbacks and block expressions.
51///
52/// # Arguments
53/// * `expr` - The expression to evaluate
54/// * `global_vars` - Global variable storage (executor's variables)
55/// * `local_vars` - Local scope variables (callback parameters, block bindings)
56///
57/// # Returns
58/// The evaluated JSON value or an execution error.
59pub fn evaluate_with_scope<V: VariableProvider>(
60    expr: &ValueExpr,
61    global_vars: &V,
62    local_vars: &HashMap<String, JsonValue>,
63) -> Result<JsonValue, ExecutionError> {
64    match expr {
65        ValueExpr::Variable(name) => evaluate_variable_lookup(name, global_vars, local_vars),
66        ValueExpr::Literal(value) => Ok(value.clone()),
67        ValueExpr::PropertyAccess { object, property } => {
68            evaluate_property_access(object, property, global_vars, local_vars)
69        },
70        ValueExpr::ArrayIndex { array, index } => {
71            evaluate_array_index(array, index, global_vars, local_vars)
72        },
73        ValueExpr::ObjectLiteral { fields } => {
74            evaluate_object_literal(fields, global_vars, local_vars)
75        },
76        ValueExpr::ArrayLiteral { items } => evaluate_array_literal(items, global_vars, local_vars),
77        ValueExpr::BinaryOp { left, op, right } => {
78            let l = evaluate_with_scope(left, global_vars, local_vars)?;
79            let r = evaluate_with_scope(right, global_vars, local_vars)?;
80            evaluate_binary_op(&l, *op, &r)
81        },
82        ValueExpr::UnaryOp { op, operand } => {
83            let v = evaluate_with_scope(operand, global_vars, local_vars)?;
84            evaluate_unary_op(*op, &v)
85        },
86        ValueExpr::Ternary {
87            condition,
88            consequent,
89            alternate,
90        } => evaluate_ternary(condition, consequent, alternate, global_vars, local_vars),
91        ValueExpr::OptionalChain { object, property } => {
92            evaluate_optional_chain(object, property, global_vars, local_vars)
93        },
94        ValueExpr::NullishCoalesce { left, right } => {
95            evaluate_nullish_coalesce(left, right, global_vars, local_vars)
96        },
97        ValueExpr::ArrayMethod { array, method } => {
98            evaluate_array_method_dispatch(array, method, global_vars, local_vars)
99        },
100        ValueExpr::NumberMethod { number, method } => {
101            let num_value = evaluate_with_scope(number, global_vars, local_vars)?;
102            evaluate_number_method(&num_value, method)
103        },
104        ValueExpr::Block { bindings, result } => {
105            evaluate_block(bindings, result, global_vars, local_vars)
106        },
107        ValueExpr::BuiltinCall { func, args } => {
108            evaluate_builtin_call(func, args, global_vars, local_vars)
109        },
110        ValueExpr::ApiCall { .. } => Err(executor_only_error(
111            "API calls should be handled by executor, not evaluator",
112        )),
113        ValueExpr::Await { .. } => Err(executor_only_error(
114            "Await expressions should be handled by executor",
115        )),
116        ValueExpr::PromiseAll { .. } => Err(executor_only_error(
117            "Promise.all should be handled by executor",
118        )),
119        #[cfg(feature = "mcp-code-mode")]
120        ValueExpr::McpCall { .. } => Err(executor_only_error(
121            "MCP calls should be handled by executor, not evaluator",
122        )),
123        ValueExpr::SdkCall { .. } => Err(executor_only_error(
124            "SDK calls should be handled by executor, not evaluator",
125        )),
126    }
127}
128
129/// Build a "should be handled by executor" runtime error.
130#[inline]
131fn executor_only_error(message: &str) -> ExecutionError {
132    ExecutionError::RuntimeError {
133        message: message.into(),
134    }
135}
136
137/// Evaluate a `Variable` reference: local scope, then global, then JS built-ins.
138fn evaluate_variable_lookup<V: VariableProvider>(
139    name: &str,
140    global_vars: &V,
141    local_vars: &HashMap<String, JsonValue>,
142) -> Result<JsonValue, ExecutionError> {
143    if let Some(value) = local_vars.get(name) {
144        return Ok(value.clone());
145    }
146    if let Some(value) = global_vars.get_variable(name) {
147        return Ok(value.clone());
148    }
149    // JavaScript built-in globals — `undefined` maps to JSON null (no JSON distinction).
150    if name == "undefined" {
151        return Ok(JsonValue::Null);
152    }
153    Err(ExecutionError::RuntimeError {
154        message: format!("Undefined variable: {}", name),
155    })
156}
157
158/// Evaluate `obj.property`. Non-objects produce `null`.
159fn evaluate_property_access<V: VariableProvider>(
160    object: &ValueExpr,
161    property: &str,
162    global_vars: &V,
163    local_vars: &HashMap<String, JsonValue>,
164) -> Result<JsonValue, ExecutionError> {
165    let obj = evaluate_with_scope(object, global_vars, local_vars)?;
166    match obj {
167        JsonValue::Object(map) => Ok(map.get(property).cloned().unwrap_or(JsonValue::Null)),
168        _ => Ok(JsonValue::Null),
169    }
170}
171
172/// Evaluate `arr[index]`. Non-array/non-number combinations produce `null`.
173fn evaluate_array_index<V: VariableProvider>(
174    array: &ValueExpr,
175    index: &ValueExpr,
176    global_vars: &V,
177    local_vars: &HashMap<String, JsonValue>,
178) -> Result<JsonValue, ExecutionError> {
179    let arr = evaluate_with_scope(array, global_vars, local_vars)?;
180    let idx = evaluate_with_scope(index, global_vars, local_vars)?;
181    match (arr, idx) {
182        (JsonValue::Array(arr), JsonValue::Number(n)) => {
183            let i = n.as_i64().unwrap_or(0) as usize;
184            Ok(arr.get(i).cloned().unwrap_or(JsonValue::Null))
185        },
186        _ => Ok(JsonValue::Null),
187    }
188}
189
190/// Evaluate `{ key: value, ...spread }`. Non-object spreads are no-ops (JS semantics).
191fn evaluate_object_literal<V: VariableProvider>(
192    fields: &[ObjectField],
193    global_vars: &V,
194    local_vars: &HashMap<String, JsonValue>,
195) -> Result<JsonValue, ExecutionError> {
196    let mut map = serde_json::Map::new();
197    for field in fields {
198        evaluate_object_field_into(field, &mut map, global_vars, local_vars)?;
199    }
200    Ok(JsonValue::Object(map))
201}
202
203/// Evaluate one `ObjectField` (key/value or spread) and merge into `map`.
204fn evaluate_object_field_into<V: VariableProvider>(
205    field: &ObjectField,
206    map: &mut serde_json::Map<String, JsonValue>,
207    global_vars: &V,
208    local_vars: &HashMap<String, JsonValue>,
209) -> Result<(), ExecutionError> {
210    match field {
211        ObjectField::KeyValue {
212            key,
213            value: value_expr,
214        } => {
215            let value = evaluate_with_scope(value_expr, global_vars, local_vars)?;
216            map.insert(key.clone(), value);
217        },
218        ObjectField::Spread { expr } => {
219            let spread_val = evaluate_with_scope(expr, global_vars, local_vars)?;
220            if let JsonValue::Object(spread_map) = spread_val {
221                for (k, v) in spread_map {
222                    map.insert(k, v);
223                }
224            }
225            // Non-objects spread as no-op (matches JS behavior)
226        },
227    }
228    Ok(())
229}
230
231/// Evaluate `[item1, item2, ...]` left-to-right.
232fn evaluate_array_literal<V: VariableProvider>(
233    items: &[ValueExpr],
234    global_vars: &V,
235    local_vars: &HashMap<String, JsonValue>,
236) -> Result<JsonValue, ExecutionError> {
237    let mut arr = Vec::with_capacity(items.len());
238    for item in items {
239        arr.push(evaluate_with_scope(item, global_vars, local_vars)?);
240    }
241    Ok(JsonValue::Array(arr))
242}
243
244/// Evaluate `condition ? consequent : alternate` using JS truthiness on the condition.
245fn evaluate_ternary<V: VariableProvider>(
246    condition: &ValueExpr,
247    consequent: &ValueExpr,
248    alternate: &ValueExpr,
249    global_vars: &V,
250    local_vars: &HashMap<String, JsonValue>,
251) -> Result<JsonValue, ExecutionError> {
252    let cond = evaluate_with_scope(condition, global_vars, local_vars)?;
253    if is_truthy(&cond) {
254        evaluate_with_scope(consequent, global_vars, local_vars)
255    } else {
256        evaluate_with_scope(alternate, global_vars, local_vars)
257    }
258}
259
260/// Evaluate `obj?.property`. Null/undefined short-circuits to `null`.
261fn evaluate_optional_chain<V: VariableProvider>(
262    object: &ValueExpr,
263    property: &str,
264    global_vars: &V,
265    local_vars: &HashMap<String, JsonValue>,
266) -> Result<JsonValue, ExecutionError> {
267    let obj = evaluate_with_scope(object, global_vars, local_vars)?;
268    match obj {
269        JsonValue::Null => Ok(JsonValue::Null),
270        JsonValue::Object(map) => Ok(map.get(property).cloned().unwrap_or(JsonValue::Null)),
271        _ => Ok(JsonValue::Null),
272    }
273}
274
275/// Evaluate `a ?? b`: take `a` unless it is null/undefined, in which case evaluate `b`.
276fn evaluate_nullish_coalesce<V: VariableProvider>(
277    left: &ValueExpr,
278    right: &ValueExpr,
279    global_vars: &V,
280    local_vars: &HashMap<String, JsonValue>,
281) -> Result<JsonValue, ExecutionError> {
282    let l = evaluate_with_scope(left, global_vars, local_vars)?;
283    if is_nullish(&l) {
284        evaluate_with_scope(right, global_vars, local_vars)
285    } else {
286        Ok(l)
287    }
288}
289
290/// Dispatch to `evaluate_array_method_with_scope` after evaluating the receiver and
291/// cloning local scope once (vs N clones per element inside the method body).
292fn evaluate_array_method_dispatch<V: VariableProvider>(
293    array: &ValueExpr,
294    method: &ArrayMethodCall,
295    global_vars: &V,
296    local_vars: &HashMap<String, JsonValue>,
297) -> Result<JsonValue, ExecutionError> {
298    let arr_value = evaluate_with_scope(array, global_vars, local_vars)?;
299    let mut scope = local_vars.clone();
300    evaluate_array_method_with_scope(&arr_value, method, global_vars, &mut scope)
301}
302
303/// Evaluate a block expression `{ const x = ...; const y = ...; result }`.
304/// Bindings extend a freshly-merged scope in declaration order.
305fn evaluate_block<V: VariableProvider>(
306    bindings: &[(String, ValueExpr)],
307    result: &ValueExpr,
308    global_vars: &V,
309    local_vars: &HashMap<String, JsonValue>,
310) -> Result<JsonValue, ExecutionError> {
311    let mut merged_vars = local_vars.clone();
312    for (name, binding_expr) in bindings {
313        let value = evaluate_with_scope(binding_expr, global_vars, &merged_vars)?;
314        merged_vars.insert(name.clone(), value);
315    }
316    evaluate_with_scope(result, global_vars, &merged_vars)
317}
318
319/// Evaluate a `BuiltinCall`: evaluate args left-to-right then dispatch to `evaluate_builtin`.
320fn evaluate_builtin_call<V: VariableProvider>(
321    func: &BuiltinFunction,
322    args: &[ValueExpr],
323    global_vars: &V,
324    local_vars: &HashMap<String, JsonValue>,
325) -> Result<JsonValue, ExecutionError> {
326    let evaluated_args: Vec<JsonValue> = args
327        .iter()
328        .map(|a| evaluate_with_scope(a, global_vars, local_vars))
329        .collect::<Result<Vec<_>, _>>()?;
330    evaluate_builtin(func, &evaluated_args)
331}
332
333/// Evaluate a binary operation.
334pub fn evaluate_binary_op(
335    left: &JsonValue,
336    op: BinaryOperator,
337    right: &JsonValue,
338) -> Result<JsonValue, ExecutionError> {
339    match op {
340        BinaryOperator::Add => add_values(left, right),
341        BinaryOperator::Sub => numeric_op(left, right, |a, b| a - b),
342        BinaryOperator::Mul => numeric_op(left, right, |a, b| a * b),
343        BinaryOperator::Div => {
344            numeric_op(left, right, |a, b| if b != 0.0 { a / b } else { f64::NAN })
345        },
346        BinaryOperator::Mod => {
347            numeric_op(left, right, |a, b| if b != 0.0 { a % b } else { f64::NAN })
348        },
349        BinaryOperator::BitwiseOr => {
350            numeric_op(left, right, |a, b| ((a as i32) | (b as i32)) as f64)
351        },
352        BinaryOperator::Eq => Ok(JsonValue::Bool(json_equals(left, right))),
353        BinaryOperator::NotEq => Ok(JsonValue::Bool(!json_equals(left, right))),
354        BinaryOperator::StrictEq => Ok(JsonValue::Bool(json_strict_equals(left, right))),
355        BinaryOperator::StrictNotEq => Ok(JsonValue::Bool(!json_strict_equals(left, right))),
356        BinaryOperator::Lt => compare_values(left, right, |a, b| a < b),
357        BinaryOperator::Lte => compare_values(left, right, |a, b| a <= b),
358        BinaryOperator::Gt => compare_values(left, right, |a, b| a > b),
359        BinaryOperator::Gte => compare_values(left, right, |a, b| a >= b),
360        BinaryOperator::And => Ok(if is_truthy(left) {
361            right.clone()
362        } else {
363            left.clone()
364        }),
365        BinaryOperator::Or => Ok(if is_truthy(left) {
366            left.clone()
367        } else {
368            right.clone()
369        }),
370        BinaryOperator::Concat => {
371            let l_str = json_to_string(left);
372            let r_str = json_to_string(right);
373            Ok(JsonValue::String(format!("{}{}", l_str, r_str)))
374        },
375    }
376}
377
378/// Evaluate a unary operation.
379pub fn evaluate_unary_op(
380    op: UnaryOperator,
381    value: &JsonValue,
382) -> Result<JsonValue, ExecutionError> {
383    match op {
384        UnaryOperator::Not => Ok(JsonValue::Bool(!is_truthy(value))),
385        UnaryOperator::Plus => {
386            let n = to_number(value);
387            Ok(serde_json::Number::from_f64(n)
388                .map(JsonValue::Number)
389                .unwrap_or(JsonValue::Null))
390        },
391        UnaryOperator::Neg => {
392            let n = to_number(value);
393            Ok(JsonValue::Number(
394                serde_json::Number::from_f64(-n).unwrap_or_else(|| serde_json::Number::from(0)),
395            ))
396        },
397        UnaryOperator::TypeOf => {
398            let type_str = match value {
399                JsonValue::Null => "object", // JavaScript quirk
400                JsonValue::Bool(_) => "boolean",
401                JsonValue::Number(_) => "number",
402                JsonValue::String(_) => "string",
403                JsonValue::Array(_) => "object",
404                JsonValue::Object(_) => "object",
405            };
406            Ok(JsonValue::String(type_str.to_string()))
407        },
408    }
409}
410
411/// Check if a JSON value is truthy (JavaScript semantics).
412pub fn is_truthy(value: &JsonValue) -> bool {
413    match value {
414        JsonValue::Null => false,
415        JsonValue::Bool(b) => *b,
416        JsonValue::Number(n) => n.as_f64().map(|f| f != 0.0 && !f.is_nan()).unwrap_or(false),
417        JsonValue::String(s) => !s.is_empty(),
418        JsonValue::Array(_) => true,
419        JsonValue::Object(_) => true,
420    }
421}
422
423/// Check if a JSON value is nullish (null or undefined).
424pub fn is_nullish(value: &JsonValue) -> bool {
425    matches!(value, JsonValue::Null)
426}
427
428/// Convert a JSON value to a number (JavaScript semantics).
429pub fn to_number(value: &JsonValue) -> f64 {
430    match value {
431        JsonValue::Null => 0.0,
432        JsonValue::Bool(b) => {
433            if *b {
434                1.0
435            } else {
436                0.0
437            }
438        },
439        JsonValue::Number(n) => n.as_f64().unwrap_or(f64::NAN),
440        JsonValue::String(s) => s.parse().unwrap_or(f64::NAN),
441        JsonValue::Array(_) | JsonValue::Object(_) => f64::NAN,
442    }
443}
444
445/// Add two JSON values (handles both numeric and string concatenation).
446fn add_values(left: &JsonValue, right: &JsonValue) -> Result<JsonValue, ExecutionError> {
447    // String concatenation takes precedence
448    if matches!(left, JsonValue::String(_)) || matches!(right, JsonValue::String(_)) {
449        let l_str = json_to_string(left);
450        let r_str = json_to_string(right);
451        return Ok(JsonValue::String(format!("{}{}", l_str, r_str)));
452    }
453
454    // Numeric addition
455    let l = to_number(left);
456    let r = to_number(right);
457    Ok(JsonValue::Number(
458        serde_json::Number::from_f64(l + r).unwrap_or_else(|| serde_json::Number::from(0)),
459    ))
460}
461
462/// Perform a numeric operation.
463fn numeric_op<F>(left: &JsonValue, right: &JsonValue, op: F) -> Result<JsonValue, ExecutionError>
464where
465    F: Fn(f64, f64) -> f64,
466{
467    let l = to_number(left);
468    let r = to_number(right);
469    let result = op(l, r);
470    Ok(JsonValue::Number(
471        serde_json::Number::from_f64(result).unwrap_or_else(|| serde_json::Number::from(0)),
472    ))
473}
474
475/// Compare two JSON values.
476fn compare_values<F>(
477    left: &JsonValue,
478    right: &JsonValue,
479    op: F,
480) -> Result<JsonValue, ExecutionError>
481where
482    F: Fn(f64, f64) -> bool,
483{
484    let l = to_number(left);
485    let r = to_number(right);
486    Ok(JsonValue::Bool(op(l, r)))
487}
488
489/// Check equality (loose, ==).
490fn json_equals(left: &JsonValue, right: &JsonValue) -> bool {
491    match (left, right) {
492        (JsonValue::Null, JsonValue::Null) => true,
493        (JsonValue::Bool(a), JsonValue::Bool(b)) => a == b,
494        (JsonValue::Number(a), JsonValue::Number(b)) => {
495            a.as_f64().unwrap_or(f64::NAN) == b.as_f64().unwrap_or(f64::NAN)
496        },
497        (JsonValue::String(a), JsonValue::String(b)) => a == b,
498        // Loose equality type coercion
499        (JsonValue::Number(n), JsonValue::String(s))
500        | (JsonValue::String(s), JsonValue::Number(n)) => {
501            if let Ok(parsed) = s.parse::<f64>() {
502                n.as_f64().unwrap_or(f64::NAN) == parsed
503            } else {
504                false
505            }
506        },
507        _ => false,
508    }
509}
510
511/// Check strict equality (===).
512fn json_strict_equals(left: &JsonValue, right: &JsonValue) -> bool {
513    match (left, right) {
514        (JsonValue::Null, JsonValue::Null) => true,
515        (JsonValue::Bool(a), JsonValue::Bool(b)) => a == b,
516        (JsonValue::Number(a), JsonValue::Number(b)) => {
517            a.as_f64().unwrap_or(f64::NAN) == b.as_f64().unwrap_or(f64::NAN)
518        },
519        (JsonValue::String(a), JsonValue::String(b)) => a == b,
520        (JsonValue::Array(a), JsonValue::Array(b)) => std::ptr::eq(a, b), // Reference equality
521        (JsonValue::Object(a), JsonValue::Object(b)) => std::ptr::eq(a, b), // Reference equality
522        _ => false,
523    }
524}
525
526/// Controls how non-primitive JSON values are rendered to string.
527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
528pub enum JsonStringMode {
529    /// JavaScript-compatible: objects render as "[object Object]", arrays as JSON.
530    JavaScript,
531    /// JSON-compatible: all values render via `serde_json::to_string`.
532    Json,
533}
534
535/// Convert JSON value to string with configurable object rendering.
536pub fn json_to_string_with_mode(value: &JsonValue, mode: JsonStringMode) -> String {
537    match value {
538        JsonValue::Null => "null".to_string(),
539        JsonValue::Bool(b) => b.to_string(),
540        JsonValue::Number(n) => n.to_string(),
541        JsonValue::String(s) => s.clone(),
542        JsonValue::Array(_) => match mode {
543            JsonStringMode::JavaScript => value.to_string(),
544            JsonStringMode::Json => serde_json::to_string(value).unwrap_or_default(),
545        },
546        JsonValue::Object(_) => match mode {
547            JsonStringMode::JavaScript => "[object Object]".to_string(),
548            JsonStringMode::Json => serde_json::to_string(value).unwrap_or_default(),
549        },
550    }
551}
552
553/// Convert JSON value to string (JavaScript-compatible mode).
554///
555/// Objects render as `"[object Object]"` matching JavaScript's `String()` behavior.
556pub fn json_to_string(value: &JsonValue) -> String {
557    json_to_string_with_mode(value, JsonStringMode::JavaScript)
558}
559
560/// Restore a scope variable to its previous value, or remove it if it didn't exist before.
561#[inline]
562fn restore_scope_var(
563    scope: &mut HashMap<String, JsonValue>,
564    key: &str,
565    previous: Option<JsonValue>,
566) {
567    match previous {
568        Some(prev) => {
569            scope.insert(key.to_string(), prev);
570        },
571        None => {
572            scope.remove(key);
573        },
574    }
575}
576
577/// Evaluate an array method with scope.
578///
579/// Supports dual-dispatch: methods like `.length`, `.includes()`, `.indexOf()`,
580/// `.slice()`, and `.concat()` work on both arrays and strings (matching JS behavior).
581/// Array-only methods (`.map()`, `.filter()`, etc.) produce a clear error on strings.
582pub fn evaluate_array_method_with_scope<V: VariableProvider>(
583    arr_value: &JsonValue,
584    method: &ArrayMethodCall,
585    global_vars: &V,
586    local_vars: &mut HashMap<String, JsonValue>,
587) -> Result<JsonValue, ExecutionError> {
588    // String-compatible methods — dispatch before array handling.
589    if let JsonValue::String(s) = arr_value {
590        return evaluate_string_method(s, method, global_vars, local_vars);
591    }
592
593    let arr = match arr_value {
594        JsonValue::Array(a) => a.clone(),
595        _ => {
596            return Err(ExecutionError::RuntimeError {
597                message: "Method called on non-array and non-string".into(),
598            })
599        },
600    };
601
602    match method {
603        ArrayMethodCall::Length => Ok(JsonValue::Number((arr.len() as i64).into())),
604        ArrayMethodCall::Map { item_var, body } => {
605            eval_array_map(arr, item_var, body, global_vars, local_vars)
606        },
607        ArrayMethodCall::Filter {
608            item_var,
609            predicate,
610        } => eval_array_filter(arr, item_var, predicate, global_vars, local_vars),
611        ArrayMethodCall::Find {
612            item_var,
613            predicate,
614        } => eval_array_find(arr, item_var, predicate, global_vars, local_vars),
615        ArrayMethodCall::Some {
616            item_var,
617            predicate,
618        } => eval_array_some(arr, item_var, predicate, global_vars, local_vars),
619        ArrayMethodCall::Every {
620            item_var,
621            predicate,
622        } => eval_array_every(arr, item_var, predicate, global_vars, local_vars),
623        ArrayMethodCall::FlatMap { item_var, body } => {
624            eval_array_flat_map(arr, item_var, body, global_vars, local_vars)
625        },
626        ArrayMethodCall::Reduce {
627            acc_var,
628            item_var,
629            body,
630            initial,
631        } => eval_array_reduce(
632            arr,
633            acc_var,
634            item_var,
635            body,
636            initial,
637            global_vars,
638            local_vars,
639        ),
640        ArrayMethodCall::Slice { start, end } => Ok(eval_array_slice(arr, *start, *end)),
641        ArrayMethodCall::Concat { other } => eval_array_concat(arr, other, global_vars, local_vars),
642        ArrayMethodCall::Push { item } => eval_array_push(arr, item, global_vars, local_vars),
643        ArrayMethodCall::Join { separator } => Ok(eval_array_join(&arr, separator.as_deref())),
644        ArrayMethodCall::Reverse => {
645            let mut reversed = arr;
646            reversed.reverse();
647            Ok(JsonValue::Array(reversed))
648        },
649        ArrayMethodCall::Sort { comparator } => {
650            eval_array_sort(arr, comparator.as_ref(), global_vars, local_vars)
651        },
652        ArrayMethodCall::Flat => Ok(JsonValue::Array(flatten_array(arr, 1))),
653        ArrayMethodCall::Includes { item } => {
654            eval_array_includes(&arr, item, global_vars, local_vars)
655        },
656        ArrayMethodCall::IndexOf { item } => {
657            eval_array_index_of(&arr, item, global_vars, local_vars)
658        },
659        ArrayMethodCall::First => Ok(arr.into_iter().next().unwrap_or(JsonValue::Null)),
660        ArrayMethodCall::Last => Ok(arr.into_iter().last().unwrap_or(JsonValue::Null)),
661        ArrayMethodCall::ToString => Ok(JsonValue::String(
662            serde_json::to_string(&JsonValue::Array(arr)).unwrap_or_default(),
663        )),
664
665        // String-only methods — error when called on arrays.
666        ArrayMethodCall::ToLowerCase
667        | ArrayMethodCall::ToUpperCase
668        | ArrayMethodCall::StartsWith { .. }
669        | ArrayMethodCall::EndsWith { .. }
670        | ArrayMethodCall::Trim
671        | ArrayMethodCall::Replace { .. }
672        | ArrayMethodCall::Split { .. }
673        | ArrayMethodCall::Substring { .. } => Err(ExecutionError::RuntimeError {
674            message: "This method is only available on strings, not arrays".into(),
675        }),
676    }
677}
678
679/// `arr.map(item => body)` — collect each evaluated body into a result vec.
680fn eval_array_map<V: VariableProvider>(
681    arr: Vec<JsonValue>,
682    item_var: &str,
683    body: &ValueExpr,
684    global_vars: &V,
685    local_vars: &mut HashMap<String, JsonValue>,
686) -> Result<JsonValue, ExecutionError> {
687    let mut results = Vec::with_capacity(arr.len());
688    for item in arr {
689        let old = local_vars.insert(item_var.to_string(), item);
690        let value = evaluate_with_scope(body, global_vars, local_vars);
691        restore_scope_var(local_vars, item_var, old);
692        results.push(value?);
693    }
694    Ok(JsonValue::Array(results))
695}
696
697/// `arr.filter(item => predicate)` — keep items whose predicate is truthy.
698fn eval_array_filter<V: VariableProvider>(
699    arr: Vec<JsonValue>,
700    item_var: &str,
701    predicate: &ValueExpr,
702    global_vars: &V,
703    local_vars: &mut HashMap<String, JsonValue>,
704) -> Result<JsonValue, ExecutionError> {
705    let mut results = Vec::new();
706    for item in arr {
707        let old = local_vars.insert(item_var.to_string(), item.clone());
708        let keep = evaluate_with_scope(predicate, global_vars, local_vars);
709        restore_scope_var(local_vars, item_var, old);
710        if is_truthy(&keep?) {
711            results.push(item);
712        }
713    }
714    Ok(JsonValue::Array(results))
715}
716
717/// `arr.find(item => predicate)` — first item whose predicate is truthy, or null.
718fn eval_array_find<V: VariableProvider>(
719    arr: Vec<JsonValue>,
720    item_var: &str,
721    predicate: &ValueExpr,
722    global_vars: &V,
723    local_vars: &mut HashMap<String, JsonValue>,
724) -> Result<JsonValue, ExecutionError> {
725    for item in arr {
726        let old = local_vars.insert(item_var.to_string(), item.clone());
727        let found = evaluate_with_scope(predicate, global_vars, local_vars);
728        restore_scope_var(local_vars, item_var, old);
729        if is_truthy(&found?) {
730            return Ok(item);
731        }
732    }
733    Ok(JsonValue::Null)
734}
735
736/// `arr.some(item => predicate)` — true iff any item's predicate is truthy.
737fn eval_array_some<V: VariableProvider>(
738    arr: Vec<JsonValue>,
739    item_var: &str,
740    predicate: &ValueExpr,
741    global_vars: &V,
742    local_vars: &mut HashMap<String, JsonValue>,
743) -> Result<JsonValue, ExecutionError> {
744    for item in arr {
745        let old = local_vars.insert(item_var.to_string(), item);
746        let found = evaluate_with_scope(predicate, global_vars, local_vars);
747        restore_scope_var(local_vars, item_var, old);
748        if is_truthy(&found?) {
749            return Ok(JsonValue::Bool(true));
750        }
751    }
752    Ok(JsonValue::Bool(false))
753}
754
755/// `arr.every(item => predicate)` — true iff every item's predicate is truthy.
756fn eval_array_every<V: VariableProvider>(
757    arr: Vec<JsonValue>,
758    item_var: &str,
759    predicate: &ValueExpr,
760    global_vars: &V,
761    local_vars: &mut HashMap<String, JsonValue>,
762) -> Result<JsonValue, ExecutionError> {
763    for item in arr {
764        let old = local_vars.insert(item_var.to_string(), item);
765        let found = evaluate_with_scope(predicate, global_vars, local_vars);
766        restore_scope_var(local_vars, item_var, old);
767        if !is_truthy(&found?) {
768            return Ok(JsonValue::Bool(false));
769        }
770    }
771    Ok(JsonValue::Bool(true))
772}
773
774/// `arr.flatMap(item => body)` — map then flatten one level (non-array bodies kept as-is).
775fn eval_array_flat_map<V: VariableProvider>(
776    arr: Vec<JsonValue>,
777    item_var: &str,
778    body: &ValueExpr,
779    global_vars: &V,
780    local_vars: &mut HashMap<String, JsonValue>,
781) -> Result<JsonValue, ExecutionError> {
782    let mut results = Vec::new();
783    for item in arr {
784        let old = local_vars.insert(item_var.to_string(), item);
785        let value = evaluate_with_scope(body, global_vars, local_vars);
786        restore_scope_var(local_vars, item_var, old);
787        match value? {
788            JsonValue::Array(items) => results.extend(items),
789            other => results.push(other),
790        }
791    }
792    Ok(JsonValue::Array(results))
793}
794
795/// `arr.reduce((acc, item) => body, initial)` — left-fold with both vars in scope.
796fn eval_array_reduce<V: VariableProvider>(
797    arr: Vec<JsonValue>,
798    acc_var: &str,
799    item_var: &str,
800    body: &ValueExpr,
801    initial: &ValueExpr,
802    global_vars: &V,
803    local_vars: &mut HashMap<String, JsonValue>,
804) -> Result<JsonValue, ExecutionError> {
805    let mut acc = evaluate_with_scope(initial, global_vars, local_vars)?;
806    for item in arr {
807        let old_acc = local_vars.insert(acc_var.to_string(), acc.clone());
808        let old_item = local_vars.insert(item_var.to_string(), item);
809        let result = evaluate_with_scope(body, global_vars, local_vars);
810        restore_scope_var(local_vars, acc_var, old_acc);
811        restore_scope_var(local_vars, item_var, old_item);
812        acc = result?;
813    }
814    Ok(acc)
815}
816
817/// Clamp a half-open slice `[start, end)` to a known length.
818/// Returns `(skip, take)` for `Iterator::skip().take()`. `end` defaults to `len`.
819fn clamp_slice_bounds(len: usize, start: usize, end: Option<usize>) -> (usize, usize) {
820    let start = start.min(len);
821    let end = end.unwrap_or(len).min(len);
822    (start, end.saturating_sub(start))
823}
824
825/// `arr.slice(start, end)` — half-open interval, `end` defaults to `len`.
826fn eval_array_slice(arr: Vec<JsonValue>, start: usize, end: Option<usize>) -> JsonValue {
827    let (skip, take) = clamp_slice_bounds(arr.len(), start, end);
828    let sliced: Vec<JsonValue> = arr.into_iter().skip(skip).take(take).collect();
829    JsonValue::Array(sliced)
830}
831
832/// `arr.concat(other)` — array → extend in place; non-array → push as single element.
833fn eval_array_concat<V: VariableProvider>(
834    arr: Vec<JsonValue>,
835    other: &ValueExpr,
836    global_vars: &V,
837    local_vars: &mut HashMap<String, JsonValue>,
838) -> Result<JsonValue, ExecutionError> {
839    let mut result = arr;
840    let other_val = evaluate_with_scope(other, global_vars, local_vars)?;
841    if let JsonValue::Array(other_arr) = other_val {
842        result.extend(other_arr);
843    } else {
844        result.push(other_val);
845    }
846    Ok(JsonValue::Array(result))
847}
848
849/// `arr.push(item)` — append, returning a new array (no in-place mutation of source).
850fn eval_array_push<V: VariableProvider>(
851    arr: Vec<JsonValue>,
852    item: &ValueExpr,
853    global_vars: &V,
854    local_vars: &mut HashMap<String, JsonValue>,
855) -> Result<JsonValue, ExecutionError> {
856    let mut result = arr;
857    let item_val = evaluate_with_scope(item, global_vars, local_vars)?;
858    result.push(item_val);
859    Ok(JsonValue::Array(result))
860}
861
862/// `arr.join(separator)` — JS-compatible string join, `","` if separator is `None`.
863fn eval_array_join(arr: &[JsonValue], separator: Option<&str>) -> JsonValue {
864    let sep = separator.unwrap_or(",");
865    let joined: String = arr.iter().map(json_to_string).collect::<Vec<_>>().join(sep);
866    JsonValue::String(joined)
867}
868
869/// `arr.sort()` (default lexicographic) or `arr.sort((a, b) => expr)` (custom comparator).
870/// Custom-comparator errors are captured on the first failing pair and bubbled out.
871fn eval_array_sort<V: VariableProvider>(
872    arr: Vec<JsonValue>,
873    comparator: Option<&(String, String, Box<ValueExpr>)>,
874    global_vars: &V,
875    local_vars: &HashMap<String, JsonValue>,
876) -> Result<JsonValue, ExecutionError> {
877    let mut sorted = arr;
878    match comparator {
879        None => sorted.sort_by_key(json_to_string),
880        Some((a_var, b_var, body)) => {
881            sort_with_comparator(&mut sorted, a_var, b_var, body, global_vars, local_vars)?;
882        },
883    }
884    Ok(JsonValue::Array(sorted))
885}
886
887/// Apply a JavaScript-style comparator callback to a `Vec<JsonValue>` in place.
888/// Captures the first comparator error and propagates it after the sort completes.
889fn sort_with_comparator<V: VariableProvider>(
890    sorted: &mut [JsonValue],
891    a_var: &str,
892    b_var: &str,
893    body: &ValueExpr,
894    global_vars: &V,
895    local_vars: &HashMap<String, JsonValue>,
896) -> Result<(), ExecutionError> {
897    let mut sort_error: Option<ExecutionError> = None;
898    sorted.sort_by(|a, b| {
899        if sort_error.is_some() {
900            return std::cmp::Ordering::Equal;
901        }
902        let mut merged = local_vars.clone();
903        merged.insert(a_var.to_string(), a.clone());
904        merged.insert(b_var.to_string(), b.clone());
905        match evaluate_with_scope(body, global_vars, &merged) {
906            Ok(result) => comparator_result_to_ordering(&result),
907            Err(e) => {
908                sort_error = Some(e);
909                std::cmp::Ordering::Equal
910            },
911        }
912    });
913    match sort_error {
914        Some(e) => Err(e),
915        None => Ok(()),
916    }
917}
918
919/// JS comparator convention: negative → Less, positive → Greater, zero/NaN → Equal.
920#[inline]
921fn comparator_result_to_ordering(result: &JsonValue) -> std::cmp::Ordering {
922    let n = to_number(result);
923    if n < 0.0 {
924        std::cmp::Ordering::Less
925    } else if n > 0.0 {
926        std::cmp::Ordering::Greater
927    } else {
928        std::cmp::Ordering::Equal
929    }
930}
931
932/// `arr.includes(item)` — true iff any element loose-equals the search value.
933fn eval_array_includes<V: VariableProvider>(
934    arr: &[JsonValue],
935    item: &ValueExpr,
936    global_vars: &V,
937    local_vars: &mut HashMap<String, JsonValue>,
938) -> Result<JsonValue, ExecutionError> {
939    let search_val = evaluate_with_scope(item, global_vars, local_vars)?;
940    let found = arr.iter().any(|elem| json_equals(elem, &search_val));
941    Ok(JsonValue::Bool(found))
942}
943
944/// `arr.indexOf(item)` — first matching index, or -1.
945fn eval_array_index_of<V: VariableProvider>(
946    arr: &[JsonValue],
947    item: &ValueExpr,
948    global_vars: &V,
949    local_vars: &mut HashMap<String, JsonValue>,
950) -> Result<JsonValue, ExecutionError> {
951    let search_val = evaluate_with_scope(item, global_vars, local_vars)?;
952    for (i, arr_item) in arr.iter().enumerate() {
953        if json_equals(arr_item, &search_val) {
954            return Ok(JsonValue::Number((i as i64).into()));
955        }
956    }
957    Ok(JsonValue::Number((-1_i64).into()))
958}
959
960/// Evaluate a method call on a string value.
961///
962/// These mirror JavaScript's string methods for the subset that overlaps
963/// with array methods (`.length`, `.includes()`, `.indexOf()`, `.slice()`, `.concat()`).
964fn evaluate_string_method<V: VariableProvider>(
965    s: &str,
966    method: &ArrayMethodCall,
967    global_vars: &V,
968    local_vars: &HashMap<String, JsonValue>,
969) -> Result<JsonValue, ExecutionError> {
970    match method {
971        ArrayMethodCall::Length => Ok(JsonValue::Number((s.chars().count() as i64).into())),
972        ArrayMethodCall::Includes { item } => {
973            eval_string_bool_predicate(s, item, global_vars, local_vars, |s, sub| s.contains(sub))
974        },
975        ArrayMethodCall::IndexOf { item } => eval_string_index_of(s, item, global_vars, local_vars),
976        ArrayMethodCall::Slice { start, end } => Ok(eval_string_slice(s, *start, *end)),
977        ArrayMethodCall::Concat { other } => eval_string_concat(s, other, global_vars, local_vars),
978        ArrayMethodCall::ToLowerCase => Ok(JsonValue::String(s.to_lowercase())),
979        ArrayMethodCall::ToUpperCase => Ok(JsonValue::String(s.to_uppercase())),
980        ArrayMethodCall::StartsWith { search } => {
981            eval_string_bool_predicate(s, search, global_vars, local_vars, |s, sub| {
982                s.starts_with(sub)
983            })
984        },
985        ArrayMethodCall::EndsWith { search } => {
986            eval_string_bool_predicate(s, search, global_vars, local_vars, |s, sub| {
987                s.ends_with(sub)
988            })
989        },
990        ArrayMethodCall::Trim => Ok(JsonValue::String(s.trim().to_string())),
991        ArrayMethodCall::Replace {
992            search,
993            replacement,
994        } => eval_string_replace(s, search, replacement, global_vars, local_vars),
995        ArrayMethodCall::Split { separator } => {
996            eval_string_split(s, separator, global_vars, local_vars)
997        },
998        ArrayMethodCall::Substring { start, end } => {
999            eval_string_substring(s, start, end.as_deref(), global_vars, local_vars)
1000        },
1001        ArrayMethodCall::ToString => Ok(JsonValue::String(s.to_string())),
1002
1003        // Array-only methods — produce a helpful error.
1004        _ => Err(ExecutionError::RuntimeError {
1005            message: format!(
1006                "String does not support {} — use it only on arrays",
1007                array_only_method_label(method)
1008            ),
1009        }),
1010    }
1011}
1012
1013/// Pretty-name array-only methods for use in the "string does not support …" error.
1014fn array_only_method_label(method: &ArrayMethodCall) -> &'static str {
1015    match method {
1016        ArrayMethodCall::Map { .. } => ".map()",
1017        ArrayMethodCall::Filter { .. } => ".filter()",
1018        ArrayMethodCall::Find { .. } => ".find()",
1019        ArrayMethodCall::Some { .. } => ".some()",
1020        ArrayMethodCall::Every { .. } => ".every()",
1021        ArrayMethodCall::Reduce { .. } => ".reduce()",
1022        ArrayMethodCall::FlatMap { .. } => ".flatMap()",
1023        ArrayMethodCall::Push { .. } => ".push()",
1024        ArrayMethodCall::Join { .. } => ".join()",
1025        ArrayMethodCall::Reverse => ".reverse()",
1026        ArrayMethodCall::Sort { .. } => ".sort()",
1027        ArrayMethodCall::Flat => ".flat()",
1028        ArrayMethodCall::First => ".first()",
1029        ArrayMethodCall::Last => ".last()",
1030        _ => "this method",
1031    }
1032}
1033
1034/// Run a `&str -> &str -> bool` predicate against an evaluated needle, returning
1035/// `Bool(false)` for non-string args. Shared by `includes`, `startsWith`, `endsWith`.
1036fn eval_string_bool_predicate<V, P>(
1037    s: &str,
1038    needle: &ValueExpr,
1039    global_vars: &V,
1040    local_vars: &HashMap<String, JsonValue>,
1041    predicate: P,
1042) -> Result<JsonValue, ExecutionError>
1043where
1044    V: VariableProvider,
1045    P: FnOnce(&str, &str) -> bool,
1046{
1047    let search_val = evaluate_with_scope(needle, global_vars, local_vars)?;
1048    match search_val {
1049        JsonValue::String(sub) => Ok(JsonValue::Bool(predicate(s, sub.as_str()))),
1050        _ => Ok(JsonValue::Bool(false)),
1051    }
1052}
1053
1054/// `s.indexOf(sub)` — char-based index for multi-byte safety; -1 on miss / non-string arg.
1055fn eval_string_index_of<V: VariableProvider>(
1056    s: &str,
1057    item: &ValueExpr,
1058    global_vars: &V,
1059    local_vars: &HashMap<String, JsonValue>,
1060) -> Result<JsonValue, ExecutionError> {
1061    let search_val = evaluate_with_scope(item, global_vars, local_vars)?;
1062    match search_val {
1063        JsonValue::String(sub) => {
1064            let idx = char_index_of(s, sub.as_str()).unwrap_or(-1);
1065            Ok(JsonValue::Number(idx.into()))
1066        },
1067        _ => Ok(JsonValue::Number((-1_i64).into())),
1068    }
1069}
1070
1071/// Char-based starts-at index of `needle` within `haystack` (None if missing).
1072fn char_index_of(haystack: &str, needle: &str) -> Option<i64> {
1073    haystack
1074        .char_indices()
1075        .zip(0i64..)
1076        .find_map(|((byte_pos, _), char_idx)| {
1077            if haystack[byte_pos..].starts_with(needle) {
1078                Some(char_idx)
1079            } else {
1080                None
1081            }
1082        })
1083}
1084
1085/// `s.slice(start, end)` — char-based half-open interval, clamped to length.
1086fn eval_string_slice(s: &str, start: usize, end: Option<usize>) -> JsonValue {
1087    let (skip, take) = clamp_slice_bounds(s.chars().count(), start, end);
1088    let sliced: String = s.chars().skip(skip).take(take).collect();
1089    JsonValue::String(sliced)
1090}
1091
1092/// `s.concat(other)` — JS-style coercion via json_to_string for the right-hand side.
1093fn eval_string_concat<V: VariableProvider>(
1094    s: &str,
1095    other: &ValueExpr,
1096    global_vars: &V,
1097    local_vars: &HashMap<String, JsonValue>,
1098) -> Result<JsonValue, ExecutionError> {
1099    let other_val = evaluate_with_scope(other, global_vars, local_vars)?;
1100    Ok(JsonValue::String(format!(
1101        "{}{}",
1102        s,
1103        json_to_string(&other_val)
1104    )))
1105}
1106
1107/// `s.replace(needle, repl)` — JS replaces only the first occurrence; non-string args
1108/// pass-through the original string unchanged.
1109fn eval_string_replace<V: VariableProvider>(
1110    s: &str,
1111    search: &ValueExpr,
1112    replacement: &ValueExpr,
1113    global_vars: &V,
1114    local_vars: &HashMap<String, JsonValue>,
1115) -> Result<JsonValue, ExecutionError> {
1116    let search_val = evaluate_with_scope(search, global_vars, local_vars)?;
1117    let repl_val = evaluate_with_scope(replacement, global_vars, local_vars)?;
1118    match (search_val, repl_val) {
1119        (JsonValue::String(needle), JsonValue::String(repl)) => Ok(JsonValue::String(s.replacen(
1120            needle.as_str(),
1121            repl.as_str(),
1122            1,
1123        ))),
1124        _ => Ok(JsonValue::String(s.to_string())),
1125    }
1126}
1127
1128/// `s.split(sep)` — empty separator yields per-char split (capped at 10_000), non-string
1129/// separator returns the original string wrapped in a single-element array.
1130fn eval_string_split<V: VariableProvider>(
1131    s: &str,
1132    separator: &ValueExpr,
1133    global_vars: &V,
1134    local_vars: &HashMap<String, JsonValue>,
1135) -> Result<JsonValue, ExecutionError> {
1136    let sep_val = evaluate_with_scope(separator, global_vars, local_vars)?;
1137    match sep_val {
1138        JsonValue::String(sep) if sep.is_empty() => Ok(JsonValue::Array(split_chars_capped(s))),
1139        JsonValue::String(sep) => Ok(JsonValue::Array(split_by(s, sep.as_str()))),
1140        _ => Ok(JsonValue::Array(vec![JsonValue::String(s.to_string())])),
1141    }
1142}
1143
1144/// Cap-protected char split: `"ab".split("") -> ["a", "b"]` (max 10_000 chars).
1145fn split_chars_capped(s: &str) -> Vec<JsonValue> {
1146    s.chars()
1147        .take(10_000)
1148        .map(|c| JsonValue::String(c.to_string()))
1149        .collect()
1150}
1151
1152/// `s.split(sep)` for a non-empty separator.
1153fn split_by(s: &str, sep: &str) -> Vec<JsonValue> {
1154    s.split(sep)
1155        .map(|p| JsonValue::String(p.to_string()))
1156        .collect()
1157}
1158
1159/// `s.substring(start [, end])` — JS swaps start/end if start > end; missing end means
1160/// "to end of string". Both indices are usize via lossy conversion (matches existing
1161/// pre-refactor semantics, preserved by the Wave 0 baseline).
1162fn eval_string_substring<V: VariableProvider>(
1163    s: &str,
1164    start: &ValueExpr,
1165    end: Option<&ValueExpr>,
1166    global_vars: &V,
1167    local_vars: &HashMap<String, JsonValue>,
1168) -> Result<JsonValue, ExecutionError> {
1169    let start_idx = eval_substring_index(start, global_vars, local_vars, 0)?;
1170    let end_idx = match end {
1171        Some(end_expr) => eval_substring_index(end_expr, global_vars, local_vars, usize::MAX)?,
1172        None => usize::MAX,
1173    };
1174    let (lo, hi) = if start_idx > end_idx {
1175        (end_idx, start_idx)
1176    } else {
1177        (start_idx, end_idx)
1178    };
1179    let result: String = s.chars().skip(lo).take(hi.saturating_sub(lo)).collect();
1180    Ok(JsonValue::String(result))
1181}
1182
1183/// Evaluate a substring index expression; non-numeric values fall back to `default`.
1184fn eval_substring_index<V: VariableProvider>(
1185    expr: &ValueExpr,
1186    global_vars: &V,
1187    local_vars: &HashMap<String, JsonValue>,
1188    default: usize,
1189) -> Result<usize, ExecutionError> {
1190    let val = evaluate_with_scope(expr, global_vars, local_vars)?;
1191    Ok(match val {
1192        JsonValue::Number(n) => n.as_u64().unwrap_or(0) as usize,
1193        _ => default,
1194    })
1195}
1196
1197/// Evaluate a built-in function call (parseFloat, Math.abs, Object.keys, etc.).
1198fn evaluate_builtin(
1199    func: &BuiltinFunction,
1200    args: &[JsonValue],
1201) -> Result<JsonValue, ExecutionError> {
1202    /// Helper to convert an f64 to JsonValue, returning Null for NaN.
1203    fn number_or_null(n: f64) -> JsonValue {
1204        serde_json::Number::from_f64(n)
1205            .map(JsonValue::Number)
1206            .unwrap_or(JsonValue::Null)
1207    }
1208
1209    match func {
1210        BuiltinFunction::ParseFloat | BuiltinFunction::NumberCast => {
1211            let val = args.first().unwrap_or(&JsonValue::Null);
1212            Ok(number_or_null(to_number(val)))
1213        },
1214        BuiltinFunction::ParseInt => {
1215            let val = args.first().unwrap_or(&JsonValue::Null);
1216            let n = to_number(val);
1217            if n.is_finite() {
1218                Ok(JsonValue::Number((n as i64).into()))
1219            } else {
1220                Ok(JsonValue::Null) // NaN
1221            }
1222        },
1223        BuiltinFunction::MathAbs => {
1224            let val = args.first().unwrap_or(&JsonValue::Null);
1225            Ok(number_or_null(to_number(val).abs()))
1226        },
1227        BuiltinFunction::MathRound => {
1228            let val = args.first().unwrap_or(&JsonValue::Null);
1229            Ok(number_or_null(to_number(val).round()))
1230        },
1231        BuiltinFunction::MathFloor => {
1232            let val = args.first().unwrap_or(&JsonValue::Null);
1233            Ok(number_or_null(to_number(val).floor()))
1234        },
1235        BuiltinFunction::MathCeil => {
1236            let val = args.first().unwrap_or(&JsonValue::Null);
1237            Ok(number_or_null(to_number(val).ceil()))
1238        },
1239        BuiltinFunction::MathMax => {
1240            if args.is_empty() {
1241                return Ok(number_or_null(f64::NEG_INFINITY));
1242            }
1243            let mut result = f64::NEG_INFINITY;
1244            for arg in args {
1245                result = result.max(to_number(arg));
1246            }
1247            Ok(number_or_null(result))
1248        },
1249        BuiltinFunction::MathMin => {
1250            if args.is_empty() {
1251                return Ok(number_or_null(f64::INFINITY));
1252            }
1253            let mut result = f64::INFINITY;
1254            for arg in args {
1255                result = result.min(to_number(arg));
1256            }
1257            Ok(number_or_null(result))
1258        },
1259        BuiltinFunction::ObjectKeys => {
1260            let val = args.first().unwrap_or(&JsonValue::Null);
1261            match val {
1262                JsonValue::Object(map) => {
1263                    let keys: Vec<JsonValue> =
1264                        map.keys().map(|k| JsonValue::String(k.clone())).collect();
1265                    Ok(JsonValue::Array(keys))
1266                },
1267                _ => Ok(JsonValue::Array(vec![])),
1268            }
1269        },
1270        BuiltinFunction::ObjectValues => {
1271            let val = args.first().unwrap_or(&JsonValue::Null);
1272            match val {
1273                JsonValue::Object(map) => {
1274                    let values: Vec<JsonValue> = map.values().cloned().collect();
1275                    Ok(JsonValue::Array(values))
1276                },
1277                _ => Ok(JsonValue::Array(vec![])),
1278            }
1279        },
1280        BuiltinFunction::ObjectEntries => {
1281            let val = args.first().unwrap_or(&JsonValue::Null);
1282            match val {
1283                JsonValue::Object(map) => {
1284                    let entries: Vec<JsonValue> = map
1285                        .iter()
1286                        .map(|(k, v)| {
1287                            JsonValue::Array(vec![JsonValue::String(k.clone()), v.clone()])
1288                        })
1289                        .collect();
1290                    Ok(JsonValue::Array(entries))
1291                },
1292                _ => Ok(JsonValue::Array(vec![])),
1293            }
1294        },
1295    }
1296}
1297
1298/// Flatten an array to one level.
1299fn flatten_array(arr: Vec<JsonValue>, depth: usize) -> Vec<JsonValue> {
1300    if depth == 0 {
1301        return arr;
1302    }
1303
1304    let mut result = Vec::new();
1305    for item in arr {
1306        if let JsonValue::Array(inner) = item {
1307            result.extend(flatten_array(inner, depth - 1));
1308        } else {
1309            result.push(item);
1310        }
1311    }
1312    result
1313}
1314
1315/// Evaluate a number method.
1316pub fn evaluate_number_method(
1317    num_value: &JsonValue,
1318    method: &NumberMethodCall,
1319) -> Result<JsonValue, ExecutionError> {
1320    let num = match num_value {
1321        JsonValue::Number(n) => n.as_f64().unwrap_or(0.0),
1322        JsonValue::String(s) => s.parse::<f64>().unwrap_or(0.0),
1323        _ => {
1324            return Err(ExecutionError::RuntimeError {
1325                message: format!("Number method called on non-number: {:?}", num_value),
1326            })
1327        },
1328    };
1329
1330    match method {
1331        NumberMethodCall::ToFixed { digits } => {
1332            let formatted = format!("{:.prec$}", num, prec = *digits);
1333            Ok(JsonValue::String(formatted))
1334        },
1335        NumberMethodCall::ToString => Ok(JsonValue::String(num.to_string())),
1336    }
1337}
1338
1339/// Evaluate an expression with just a variable map (no local scope).
1340/// This is a convenience wrapper for the common case.
1341pub fn evaluate(
1342    expr: &ValueExpr,
1343    variables: &HashMap<String, JsonValue>,
1344) -> Result<JsonValue, ExecutionError> {
1345    evaluate_with_scope(expr, variables, &HashMap::new())
1346}
1347
1348/// Evaluate an expression with an extra variable binding.
1349/// Creates a merged scope with the new binding and evaluates.
1350pub fn evaluate_with_binding(
1351    expr: &ValueExpr,
1352    variables: &HashMap<String, JsonValue>,
1353    var: &str,
1354    value: &JsonValue,
1355) -> Result<JsonValue, ExecutionError> {
1356    let mut local_vars = HashMap::new();
1357    local_vars.insert(var.to_string(), value.clone());
1358    evaluate_with_scope(expr, variables, &local_vars)
1359}
1360
1361/// Evaluate an expression with two extra variable bindings.
1362/// Used for reduce operations with accumulator and item variables.
1363pub fn evaluate_with_two_bindings(
1364    expr: &ValueExpr,
1365    variables: &HashMap<String, JsonValue>,
1366    var1: &str,
1367    value1: &JsonValue,
1368    var2: &str,
1369    value2: &JsonValue,
1370) -> Result<JsonValue, ExecutionError> {
1371    let mut local_vars = HashMap::new();
1372    local_vars.insert(var1.to_string(), value1.clone());
1373    local_vars.insert(var2.to_string(), value2.clone());
1374    evaluate_with_scope(expr, variables, &local_vars)
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379    use super::*;
1380
1381    #[test]
1382    fn test_undefined_global() {
1383        let vars: HashMap<String, JsonValue> = HashMap::new();
1384        let expr = ValueExpr::Variable("undefined".to_string());
1385        let result = evaluate(&expr, &vars).unwrap();
1386        assert_eq!(result, JsonValue::Null);
1387    }
1388
1389    #[test]
1390    fn test_undefined_variable_error() {
1391        let vars: HashMap<String, JsonValue> = HashMap::new();
1392        let expr = ValueExpr::Variable("nonexistent".to_string());
1393        let result = evaluate(&expr, &vars);
1394        assert!(result.is_err());
1395        match result {
1396            Err(ExecutionError::RuntimeError { message }) => {
1397                assert!(message.contains("Undefined variable"));
1398            },
1399            _ => panic!("Expected RuntimeError"),
1400        }
1401    }
1402
1403    #[test]
1404    fn test_comparison_with_undefined() {
1405        let mut vars: HashMap<String, JsonValue> = HashMap::new();
1406        vars.insert("x".to_string(), JsonValue::Null);
1407
1408        // x !== undefined should be false when x is null
1409        let expr = ValueExpr::BinaryOp {
1410            left: Box::new(ValueExpr::Variable("x".to_string())),
1411            op: BinaryOperator::StrictNotEq,
1412            right: Box::new(ValueExpr::Variable("undefined".to_string())),
1413        };
1414        let result = evaluate(&expr, &vars).unwrap();
1415        // null !== null is false (they're strictly equal in our model)
1416        assert_eq!(result, JsonValue::Bool(false));
1417    }
1418
1419    // =========================================================================
1420    // String method tests
1421    // =========================================================================
1422
1423    /// Helper to evaluate an ArrayMethod on a string variable.
1424    fn eval_string_method(s: &str, method: ArrayMethodCall) -> Result<JsonValue, ExecutionError> {
1425        let mut vars = HashMap::new();
1426        vars.insert("s".to_string(), JsonValue::String(s.to_string()));
1427        let expr = ValueExpr::ArrayMethod {
1428            array: Box::new(ValueExpr::Variable("s".to_string())),
1429            method,
1430        };
1431        evaluate(&expr, &vars)
1432    }
1433
1434    #[test]
1435    fn test_string_length() {
1436        let result = eval_string_method("hello", ArrayMethodCall::Length).unwrap();
1437        assert_eq!(result, JsonValue::Number(5.into()));
1438    }
1439
1440    #[test]
1441    fn test_string_length_empty() {
1442        let result = eval_string_method("", ArrayMethodCall::Length).unwrap();
1443        assert_eq!(result, JsonValue::Number(0.into()));
1444    }
1445
1446    #[test]
1447    fn test_string_length_multibyte() {
1448        // Emoji is 1 char, not multiple bytes
1449        let result = eval_string_method("hi\u{1F600}", ArrayMethodCall::Length).unwrap();
1450        assert_eq!(result, JsonValue::Number(3.into()));
1451    }
1452
1453    #[test]
1454    fn test_string_includes_hit() {
1455        let result = eval_string_method(
1456            "hello world",
1457            ArrayMethodCall::Includes {
1458                item: Box::new(ValueExpr::Literal(JsonValue::String("world".into()))),
1459            },
1460        )
1461        .unwrap();
1462        assert_eq!(result, JsonValue::Bool(true));
1463    }
1464
1465    #[test]
1466    fn test_string_includes_miss() {
1467        let result = eval_string_method(
1468            "hello",
1469            ArrayMethodCall::Includes {
1470                item: Box::new(ValueExpr::Literal(JsonValue::String("xyz".into()))),
1471            },
1472        )
1473        .unwrap();
1474        assert_eq!(result, JsonValue::Bool(false));
1475    }
1476
1477    #[test]
1478    fn test_string_includes_non_string_arg() {
1479        // Searching for a number in a string returns false (no coercion)
1480        let result = eval_string_method(
1481            "abc 42 def",
1482            ArrayMethodCall::Includes {
1483                item: Box::new(ValueExpr::Literal(JsonValue::Number(42.into()))),
1484            },
1485        )
1486        .unwrap();
1487        assert_eq!(result, JsonValue::Bool(false));
1488    }
1489
1490    #[test]
1491    fn test_string_index_of_found() {
1492        let result = eval_string_method(
1493            "abcdef",
1494            ArrayMethodCall::IndexOf {
1495                item: Box::new(ValueExpr::Literal(JsonValue::String("cd".into()))),
1496            },
1497        )
1498        .unwrap();
1499        assert_eq!(result, JsonValue::Number(2.into()));
1500    }
1501
1502    #[test]
1503    fn test_string_index_of_miss() {
1504        let result = eval_string_method(
1505            "abc",
1506            ArrayMethodCall::IndexOf {
1507                item: Box::new(ValueExpr::Literal(JsonValue::String("xyz".into()))),
1508            },
1509        )
1510        .unwrap();
1511        assert_eq!(result, JsonValue::Number((-1_i64).into()));
1512    }
1513
1514    #[test]
1515    fn test_string_index_of_non_string_arg() {
1516        let result = eval_string_method(
1517            "abc",
1518            ArrayMethodCall::IndexOf {
1519                item: Box::new(ValueExpr::Literal(JsonValue::Number(1.into()))),
1520            },
1521        )
1522        .unwrap();
1523        assert_eq!(result, JsonValue::Number((-1_i64).into()));
1524    }
1525
1526    #[test]
1527    fn test_string_slice() {
1528        let result = eval_string_method(
1529            "hello world",
1530            ArrayMethodCall::Slice {
1531                start: 0,
1532                end: Some(5),
1533            },
1534        )
1535        .unwrap();
1536        assert_eq!(result, JsonValue::String("hello".into()));
1537    }
1538
1539    #[test]
1540    fn test_string_slice_no_end() {
1541        let result = eval_string_method(
1542            "hello world",
1543            ArrayMethodCall::Slice {
1544                start: 6,
1545                end: None,
1546            },
1547        )
1548        .unwrap();
1549        assert_eq!(result, JsonValue::String("world".into()));
1550    }
1551
1552    #[test]
1553    fn test_string_slice_past_end() {
1554        let result = eval_string_method(
1555            "hi",
1556            ArrayMethodCall::Slice {
1557                start: 0,
1558                end: Some(100),
1559            },
1560        )
1561        .unwrap();
1562        assert_eq!(result, JsonValue::String("hi".into()));
1563    }
1564
1565    #[test]
1566    fn test_string_concat() {
1567        let result = eval_string_method(
1568            "hello",
1569            ArrayMethodCall::Concat {
1570                other: Box::new(ValueExpr::Literal(JsonValue::String(" world".into()))),
1571            },
1572        )
1573        .unwrap();
1574        assert_eq!(result, JsonValue::String("hello world".into()));
1575    }
1576
1577    #[test]
1578    fn test_string_concat_with_number() {
1579        let result = eval_string_method(
1580            "count: ",
1581            ArrayMethodCall::Concat {
1582                other: Box::new(ValueExpr::Literal(JsonValue::Number(42.into()))),
1583            },
1584        )
1585        .unwrap();
1586        assert_eq!(result, JsonValue::String("count: 42".into()));
1587    }
1588
1589    #[test]
1590    fn test_string_map_errors() {
1591        let result = eval_string_method(
1592            "hello",
1593            ArrayMethodCall::Map {
1594                item_var: "x".into(),
1595                body: Box::new(ValueExpr::Variable("x".into())),
1596            },
1597        );
1598        assert!(result.is_err());
1599        match result {
1600            Err(ExecutionError::RuntimeError { message }) => {
1601                assert!(message.contains("String does not support .map()"));
1602            },
1603            _ => panic!("Expected RuntimeError"),
1604        }
1605    }
1606
1607    #[test]
1608    fn test_string_filter_errors() {
1609        let result = eval_string_method(
1610            "hello",
1611            ArrayMethodCall::Filter {
1612                item_var: "x".into(),
1613                predicate: Box::new(ValueExpr::Literal(JsonValue::Bool(true))),
1614            },
1615        );
1616        assert!(result.is_err());
1617        match result {
1618            Err(ExecutionError::RuntimeError { message }) => {
1619                assert!(message.contains("String does not support .filter()"));
1620            },
1621            _ => panic!("Expected RuntimeError"),
1622        }
1623    }
1624
1625    #[test]
1626    fn test_array_methods_still_work_after_string_dispatch() {
1627        // Regression: ensure array .includes() still works
1628        let mut vars = HashMap::new();
1629        vars.insert(
1630            "arr".to_string(),
1631            JsonValue::Array(vec![
1632                JsonValue::Number(1.into()),
1633                JsonValue::Number(2.into()),
1634                JsonValue::Number(3.into()),
1635            ]),
1636        );
1637        let expr = ValueExpr::ArrayMethod {
1638            array: Box::new(ValueExpr::Variable("arr".to_string())),
1639            method: ArrayMethodCall::Includes {
1640                item: Box::new(ValueExpr::Literal(JsonValue::Number(2.into()))),
1641            },
1642        };
1643        let result = evaluate(&expr, &vars).unwrap();
1644        assert_eq!(result, JsonValue::Bool(true));
1645    }
1646
1647    #[test]
1648    fn test_array_length_still_works() {
1649        let mut vars = HashMap::new();
1650        vars.insert(
1651            "arr".to_string(),
1652            JsonValue::Array(vec![JsonValue::Null; 4]),
1653        );
1654        let expr = ValueExpr::ArrayMethod {
1655            array: Box::new(ValueExpr::Variable("arr".to_string())),
1656            method: ArrayMethodCall::Length,
1657        };
1658        let result = evaluate(&expr, &vars).unwrap();
1659        assert_eq!(result, JsonValue::Number(4.into()));
1660    }
1661
1662    // =========================================================================
1663    // Built-in function tests
1664    // =========================================================================
1665
1666    #[test]
1667    fn test_parse_float() {
1668        let vars = HashMap::new();
1669        let expr = ValueExpr::BuiltinCall {
1670            func: BuiltinFunction::ParseFloat,
1671            args: vec![ValueExpr::Literal(JsonValue::String("3.14".into()))],
1672        };
1673        let result = evaluate(&expr, &vars).unwrap();
1674        // Why: test fixture uses 3.14 as a representative non-integer parse target,
1675        // not the mathematical PI constant — clippy::approx_constant is a false positive here.
1676        #[allow(clippy::approx_constant)]
1677        let expected = serde_json::json!(3.14);
1678        assert_eq!(result, expected);
1679    }
1680
1681    #[test]
1682    fn test_parse_float_integer() {
1683        let vars = HashMap::new();
1684        let expr = ValueExpr::BuiltinCall {
1685            func: BuiltinFunction::ParseFloat,
1686            args: vec![ValueExpr::Literal(JsonValue::String("42".into()))],
1687        };
1688        let result = evaluate(&expr, &vars).unwrap();
1689        assert_eq!(result, serde_json::json!(42.0));
1690    }
1691
1692    #[test]
1693    fn test_parse_float_nan_returns_null() {
1694        let vars = HashMap::new();
1695        let expr = ValueExpr::BuiltinCall {
1696            func: BuiltinFunction::ParseFloat,
1697            args: vec![ValueExpr::Literal(JsonValue::String("not-a-number".into()))],
1698        };
1699        let result = evaluate(&expr, &vars).unwrap();
1700        assert_eq!(result, JsonValue::Null);
1701    }
1702
1703    #[test]
1704    fn test_parse_int() {
1705        let vars = HashMap::new();
1706        let expr = ValueExpr::BuiltinCall {
1707            func: BuiltinFunction::ParseInt,
1708            args: vec![ValueExpr::Literal(JsonValue::String("42.9".into()))],
1709        };
1710        let result = evaluate(&expr, &vars).unwrap();
1711        assert_eq!(result, serde_json::json!(42));
1712    }
1713
1714    #[test]
1715    fn test_parse_int_nan_returns_null() {
1716        let vars = HashMap::new();
1717        let expr = ValueExpr::BuiltinCall {
1718            func: BuiltinFunction::ParseInt,
1719            args: vec![ValueExpr::Literal(JsonValue::String("abc".into()))],
1720        };
1721        let result = evaluate(&expr, &vars).unwrap();
1722        assert_eq!(result, JsonValue::Null);
1723    }
1724
1725    #[test]
1726    fn test_math_abs() {
1727        let vars = HashMap::new();
1728        let expr = ValueExpr::BuiltinCall {
1729            func: BuiltinFunction::MathAbs,
1730            args: vec![ValueExpr::Literal(serde_json::json!(-5.0))],
1731        };
1732        let result = evaluate(&expr, &vars).unwrap();
1733        assert_eq!(result, serde_json::json!(5.0));
1734    }
1735
1736    #[test]
1737    fn test_math_max() {
1738        let vars = HashMap::new();
1739        let expr = ValueExpr::BuiltinCall {
1740            func: BuiltinFunction::MathMax,
1741            args: vec![
1742                ValueExpr::Literal(serde_json::json!(1)),
1743                ValueExpr::Literal(serde_json::json!(5)),
1744                ValueExpr::Literal(serde_json::json!(3)),
1745            ],
1746        };
1747        let result = evaluate(&expr, &vars).unwrap();
1748        assert_eq!(result, serde_json::json!(5.0));
1749    }
1750
1751    #[test]
1752    fn test_math_min() {
1753        let vars = HashMap::new();
1754        let expr = ValueExpr::BuiltinCall {
1755            func: BuiltinFunction::MathMin,
1756            args: vec![
1757                ValueExpr::Literal(serde_json::json!(10)),
1758                ValueExpr::Literal(serde_json::json!(2)),
1759                ValueExpr::Literal(serde_json::json!(7)),
1760            ],
1761        };
1762        let result = evaluate(&expr, &vars).unwrap();
1763        assert_eq!(result, serde_json::json!(2.0));
1764    }
1765
1766    #[test]
1767    fn test_math_round() {
1768        let vars = HashMap::new();
1769        let expr = ValueExpr::BuiltinCall {
1770            func: BuiltinFunction::MathRound,
1771            args: vec![ValueExpr::Literal(serde_json::json!(3.7))],
1772        };
1773        let result = evaluate(&expr, &vars).unwrap();
1774        assert_eq!(result, serde_json::json!(4.0));
1775    }
1776
1777    #[test]
1778    fn test_math_floor() {
1779        let vars = HashMap::new();
1780        let expr = ValueExpr::BuiltinCall {
1781            func: BuiltinFunction::MathFloor,
1782            args: vec![ValueExpr::Literal(serde_json::json!(3.7))],
1783        };
1784        let result = evaluate(&expr, &vars).unwrap();
1785        assert_eq!(result, serde_json::json!(3.0));
1786    }
1787
1788    #[test]
1789    fn test_math_ceil() {
1790        let vars = HashMap::new();
1791        let expr = ValueExpr::BuiltinCall {
1792            func: BuiltinFunction::MathCeil,
1793            args: vec![ValueExpr::Literal(serde_json::json!(3.2))],
1794        };
1795        let result = evaluate(&expr, &vars).unwrap();
1796        assert_eq!(result, serde_json::json!(4.0));
1797    }
1798
1799    #[test]
1800    fn test_object_keys() {
1801        let mut vars = HashMap::new();
1802        vars.insert("obj".to_string(), serde_json::json!({"a": 1, "b": 2}));
1803        let expr = ValueExpr::BuiltinCall {
1804            func: BuiltinFunction::ObjectKeys,
1805            args: vec![ValueExpr::Variable("obj".to_string())],
1806        };
1807        let result = evaluate(&expr, &vars).unwrap();
1808        let arr = result.as_array().unwrap();
1809        assert_eq!(arr.len(), 2);
1810        assert!(arr.contains(&JsonValue::String("a".into())));
1811        assert!(arr.contains(&JsonValue::String("b".into())));
1812    }
1813
1814    #[test]
1815    fn test_object_values() {
1816        let mut vars = HashMap::new();
1817        vars.insert("obj".to_string(), serde_json::json!({"x": 10, "y": 20}));
1818        let expr = ValueExpr::BuiltinCall {
1819            func: BuiltinFunction::ObjectValues,
1820            args: vec![ValueExpr::Variable("obj".to_string())],
1821        };
1822        let result = evaluate(&expr, &vars).unwrap();
1823        let arr = result.as_array().unwrap();
1824        assert_eq!(arr.len(), 2);
1825        assert!(arr.contains(&serde_json::json!(10)));
1826        assert!(arr.contains(&serde_json::json!(20)));
1827    }
1828
1829    #[test]
1830    fn test_object_entries() {
1831        let mut vars = HashMap::new();
1832        vars.insert("obj".to_string(), serde_json::json!({"key": "val"}));
1833        let expr = ValueExpr::BuiltinCall {
1834            func: BuiltinFunction::ObjectEntries,
1835            args: vec![ValueExpr::Variable("obj".to_string())],
1836        };
1837        let result = evaluate(&expr, &vars).unwrap();
1838        let arr = result.as_array().unwrap();
1839        assert_eq!(arr.len(), 1);
1840        assert_eq!(arr[0], serde_json::json!(["key", "val"]));
1841    }
1842
1843    #[test]
1844    fn test_object_keys_non_object() {
1845        let vars = HashMap::new();
1846        let expr = ValueExpr::BuiltinCall {
1847            func: BuiltinFunction::ObjectKeys,
1848            args: vec![ValueExpr::Literal(serde_json::json!(42))],
1849        };
1850        let result = evaluate(&expr, &vars).unwrap();
1851        assert_eq!(result, JsonValue::Array(vec![]));
1852    }
1853
1854    // =========================================================================
1855    // Unary plus tests
1856    // =========================================================================
1857
1858    #[test]
1859    fn test_unary_plus_string_to_number() {
1860        let vars = HashMap::new();
1861        let expr = ValueExpr::UnaryOp {
1862            op: UnaryOperator::Plus,
1863            operand: Box::new(ValueExpr::Literal(JsonValue::String("42".into()))),
1864        };
1865        let result = evaluate(&expr, &vars).unwrap();
1866        assert_eq!(result, serde_json::json!(42.0));
1867    }
1868
1869    #[test]
1870    fn test_unary_plus_nan_returns_null() {
1871        let vars = HashMap::new();
1872        let expr = ValueExpr::UnaryOp {
1873            op: UnaryOperator::Plus,
1874            operand: Box::new(ValueExpr::Literal(JsonValue::String("abc".into()))),
1875        };
1876        let result = evaluate(&expr, &vars).unwrap();
1877        assert_eq!(result, JsonValue::Null);
1878    }
1879
1880    // =========================================================================
1881    // Sort with comparator tests
1882    // =========================================================================
1883
1884    #[test]
1885    fn test_sort_ascending_comparator() {
1886        let mut vars = HashMap::new();
1887        vars.insert("arr".to_string(), serde_json::json!([3, 1, 4, 1, 5]));
1888        let expr = ValueExpr::ArrayMethod {
1889            array: Box::new(ValueExpr::Variable("arr".to_string())),
1890            method: ArrayMethodCall::Sort {
1891                comparator: Some((
1892                    "a".to_string(),
1893                    "b".to_string(),
1894                    Box::new(ValueExpr::BinaryOp {
1895                        left: Box::new(ValueExpr::Variable("a".to_string())),
1896                        op: BinaryOperator::Sub,
1897                        right: Box::new(ValueExpr::Variable("b".to_string())),
1898                    }),
1899                )),
1900            },
1901        };
1902        let result = evaluate(&expr, &vars).unwrap();
1903        assert_eq!(result, serde_json::json!([1, 1, 3, 4, 5]));
1904    }
1905
1906    #[test]
1907    fn test_sort_descending_comparator() {
1908        let mut vars = HashMap::new();
1909        vars.insert("arr".to_string(), serde_json::json!([3, 1, 4, 1, 5]));
1910        let expr = ValueExpr::ArrayMethod {
1911            array: Box::new(ValueExpr::Variable("arr".to_string())),
1912            method: ArrayMethodCall::Sort {
1913                comparator: Some((
1914                    "a".to_string(),
1915                    "b".to_string(),
1916                    Box::new(ValueExpr::BinaryOp {
1917                        left: Box::new(ValueExpr::Variable("b".to_string())),
1918                        op: BinaryOperator::Sub,
1919                        right: Box::new(ValueExpr::Variable("a".to_string())),
1920                    }),
1921                )),
1922            },
1923        };
1924        let result = evaluate(&expr, &vars).unwrap();
1925        assert_eq!(result, serde_json::json!([5, 4, 3, 1, 1]));
1926    }
1927
1928    #[test]
1929    fn test_sort_default_string_sort() {
1930        let mut vars = HashMap::new();
1931        vars.insert(
1932            "arr".to_string(),
1933            serde_json::json!(["banana", "apple", "cherry"]),
1934        );
1935        let expr = ValueExpr::ArrayMethod {
1936            array: Box::new(ValueExpr::Variable("arr".to_string())),
1937            method: ArrayMethodCall::Sort { comparator: None },
1938        };
1939        let result = evaluate(&expr, &vars).unwrap();
1940        assert_eq!(result, serde_json::json!(["apple", "banana", "cherry"]));
1941    }
1942
1943    // =========================================================================
1944    // Scope-chain push/pop optimization tests
1945    // =========================================================================
1946
1947    #[test]
1948    fn test_array_map_large_scope_performance() {
1949        // Regression test: verifies push/pop optimization handles large arrays
1950        // with many scope variables without excessive allocation.
1951        // Before optimization: 1000 HashMap clones (one per element).
1952        // After optimization: 1 HashMap clone (at array method entry).
1953        use std::time::Instant;
1954
1955        let mut vars = HashMap::new();
1956        // Pre-populate scope with 20 variables to make clone cost measurable
1957        for i in 0..20 {
1958            vars.insert(
1959                format!("var_{i}"),
1960                JsonValue::Number(serde_json::Number::from(i)),
1961            );
1962        }
1963
1964        // Build a 1000-element array
1965        let arr: Vec<JsonValue> = (0..1000)
1966            .map(|i| JsonValue::Number(serde_json::Number::from(i)))
1967            .collect();
1968        vars.insert("data".to_string(), JsonValue::Array(arr));
1969
1970        // Simple map: x => x (identity)
1971        let expr = ValueExpr::ArrayMethod {
1972            array: Box::new(ValueExpr::Variable("data".to_string())),
1973            method: ArrayMethodCall::Map {
1974                item_var: "x".to_string(),
1975                body: Box::new(ValueExpr::Variable("x".to_string())),
1976            },
1977        };
1978
1979        let start = Instant::now();
1980        let result = evaluate(&expr, &vars).unwrap();
1981        let elapsed = start.elapsed();
1982
1983        let arr_result = result.as_array().unwrap();
1984        assert_eq!(arr_result.len(), 1000);
1985        // Verify first and last element values
1986        assert_eq!(arr_result[0], JsonValue::Number(0.into()));
1987        assert_eq!(arr_result[999], JsonValue::Number(999.into()));
1988
1989        // Sanity check: should complete in under 100ms even on slow CI
1990        assert!(
1991            elapsed.as_millis() < 100,
1992            "Array map took {}ms, expected < 100ms",
1993            elapsed.as_millis()
1994        );
1995    }
1996
1997    #[test]
1998    fn test_array_filter_preserves_outer_scope() {
1999        // Verifies that push/pop correctly restores scope after filter.
2000        // The outer variable "x" should be unchanged after the filter runs.
2001        let mut vars = HashMap::new();
2002        vars.insert("x".to_string(), JsonValue::String("outer".into()));
2003        vars.insert(
2004            "arr".to_string(),
2005            JsonValue::Array(vec![
2006                JsonValue::Number(1.into()),
2007                JsonValue::Number(2.into()),
2008                JsonValue::Number(3.into()),
2009            ]),
2010        );
2011
2012        // Block: { filter using x as item_var, then return outer x }
2013        let expr = ValueExpr::Block {
2014            bindings: vec![(
2015                "filtered".to_string(),
2016                ValueExpr::ArrayMethod {
2017                    array: Box::new(ValueExpr::Variable("arr".to_string())),
2018                    method: ArrayMethodCall::Filter {
2019                        item_var: "x".to_string(),
2020                        predicate: Box::new(ValueExpr::BinaryOp {
2021                            left: Box::new(ValueExpr::Variable("x".to_string())),
2022                            op: BinaryOperator::Gt,
2023                            right: Box::new(ValueExpr::Literal(JsonValue::Number(1.into()))),
2024                        }),
2025                    },
2026                },
2027            )],
2028            result: Box::new(ValueExpr::Variable("x".to_string())),
2029        };
2030
2031        let result = evaluate(&expr, &vars).unwrap();
2032        // The outer "x" should still be "outer", not overwritten by the filter
2033        assert_eq!(result, JsonValue::String("outer".into()));
2034    }
2035
2036    #[test]
2037    fn test_array_reduce_large_scope_performance() {
2038        // Regression test for reduce with push/pop optimization at scale.
2039        let mut vars = HashMap::new();
2040        for i in 0..20 {
2041            vars.insert(
2042                format!("var_{i}"),
2043                JsonValue::Number(serde_json::Number::from(i)),
2044            );
2045        }
2046
2047        // Sum 1000 numbers using reduce
2048        let arr: Vec<JsonValue> = (0..1000)
2049            .map(|i| JsonValue::Number(serde_json::Number::from(i)))
2050            .collect();
2051        vars.insert("data".to_string(), JsonValue::Array(arr));
2052
2053        let expr = ValueExpr::ArrayMethod {
2054            array: Box::new(ValueExpr::Variable("data".to_string())),
2055            method: ArrayMethodCall::Reduce {
2056                acc_var: "acc".to_string(),
2057                item_var: "x".to_string(),
2058                body: Box::new(ValueExpr::BinaryOp {
2059                    left: Box::new(ValueExpr::Variable("acc".to_string())),
2060                    op: BinaryOperator::Add,
2061                    right: Box::new(ValueExpr::Variable("x".to_string())),
2062                }),
2063                initial: Box::new(ValueExpr::Literal(JsonValue::Number(0.into()))),
2064            },
2065        };
2066
2067        let result = evaluate(&expr, &vars).unwrap();
2068        // Sum of 0..1000 = 999 * 1000 / 2 = 499500
2069        // Note: the evaluator's Add operation converts to f64, so the result is 499500.0
2070        assert_eq!(result, serde_json::json!(499_500.0));
2071    }
2072}