Skip to main content

pmcp_code_mode/
executor.rs

1//! AST-based JavaScript execution for Code Mode.
2//!
3//! This module provides secure execution of validated JavaScript code by:
4//! 1. Compiling the SWC AST to an ExecutionPlan
5//! 2. Executing the plan in pure Rust (no JS runtime)
6//!
7//! ## Security Model
8//!
9//! Only operations that can be represented in the ExecutionPlan are allowed.
10//! The plan is a simple tree structure that's fully auditable before execution.
11//!
12//! ## Supported Operations
13//!
14//! - API calls: `api.get()`, `api.post()`, `api.put()`, `api.delete()`, `api.patch()`
15//! - Variable assignment: `const x = ...`
16//! - Property access: `user.id`, `response.data`
17//! - Array methods: `.map()`, `.filter()`, `.slice()`, `.find()`, `.length`
18//! - Template literals: `` `/users/${id}` ``
19//! - Object literals: `{ name: "test", id: 123 }`
20//! - Array literals: `[1, 2, 3]`
21//! - Conditionals: `if/else`
22//! - Bounded loops: `for (const x of arr.slice(0, N))`
23//! - Return statements
24
25use crate::javascript::HttpMethod;
26use crate::types::ExecutionError;
27use serde::Serialize;
28use serde_json::Value as JsonValue;
29use std::collections::{HashMap, HashSet};
30
31// Import shared evaluation functions
32use crate::eval::{
33    evaluate as shared_evaluate, evaluate_with_binding as shared_evaluate_with_binding,
34    evaluate_with_two_bindings as shared_evaluate_with_two_bindings, is_truthy as shared_is_truthy,
35    json_to_string_with_mode as shared_json_to_string_with_mode, JsonStringMode,
36};
37use swc_common::{FileName, SourceMap};
38use swc_ecma_ast::*;
39use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
40
41/// Internal control flow outcome from executing a single plan step.
42///
43/// Replaces the previous pattern of abusing `ExecutionError::LoopContinue`
44/// and `ExecutionError::LoopBreak` for non-error control flow.
45pub(crate) enum StepOutcome {
46    /// Step completed, no value produced.
47    None,
48    /// Step produced a return value (exits function/plan).
49    Return(serde_json::Value),
50    /// Loop continue signal (skip to next iteration).
51    Continue,
52    /// Loop break signal (exit current loop).
53    Break,
54}
55
56/// Configuration for execution.
57#[derive(Debug, Clone)]
58pub struct ExecutionConfig {
59    /// Maximum number of API calls allowed
60    pub max_api_calls: usize,
61    /// Maximum execution time in seconds
62    pub timeout_seconds: u64,
63    /// Maximum loop iterations
64    pub max_loop_iterations: usize,
65    /// Fields that should be filtered from API responses (internal blocklist).
66    /// These fields are stripped from responses before scripts can access them.
67    /// Field names are case-sensitive and matched at any nesting level.
68    pub blocked_fields: HashSet<String>,
69    /// Fields that cannot appear in script output (output blocklist).
70    /// These fields can be used internally but cannot be returned by the script.
71    /// Field names are case-sensitive and matched at any nesting level.
72    pub output_blocked_fields: HashSet<String>,
73}
74
75impl Default for ExecutionConfig {
76    fn default() -> Self {
77        Self {
78            max_api_calls: 50,
79            timeout_seconds: 30,
80            max_loop_iterations: 100,
81            blocked_fields: HashSet::new(),
82            output_blocked_fields: HashSet::new(),
83        }
84    }
85}
86
87impl ExecutionConfig {
88    /// Create a new config with blocked fields for API response filtering.
89    pub fn with_blocked_fields(
90        mut self,
91        fields: impl IntoIterator<Item = impl Into<String>>,
92    ) -> Self {
93        self.blocked_fields = fields.into_iter().map(Into::into).collect();
94        self
95    }
96
97    /// Create a new config with output blocked fields for return value validation.
98    pub fn with_output_blocked_fields(
99        mut self,
100        fields: impl IntoIterator<Item = impl Into<String>>,
101    ) -> Self {
102        self.output_blocked_fields = fields.into_iter().map(Into::into).collect();
103        self
104    }
105}
106
107/// Recursively filter blocked fields from a JSON value.
108///
109/// This removes any fields whose names are in the blocklist from objects,
110/// and recursively processes nested objects and arrays.
111///
112/// # Arguments
113///
114/// * `value` - The JSON value to filter
115/// * `blocked_fields` - Set of field names to remove
116///
117/// # Returns
118///
119/// A new JSON value with blocked fields removed.
120pub fn filter_blocked_fields(value: JsonValue, blocked_fields: &HashSet<String>) -> JsonValue {
121    if blocked_fields.is_empty() {
122        return value;
123    }
124
125    match value {
126        JsonValue::Object(mut map) => {
127            // Remove blocked fields at this level
128            map.retain(|key, _| !blocked_fields.contains(key));
129
130            // Recursively filter remaining values
131            let filtered: serde_json::Map<String, JsonValue> = map
132                .into_iter()
133                .map(|(k, v)| (k, filter_blocked_fields(v, blocked_fields)))
134                .collect();
135
136            JsonValue::Object(filtered)
137        },
138        JsonValue::Array(arr) => {
139            // Recursively filter each element
140            let filtered: Vec<JsonValue> = arr
141                .into_iter()
142                .map(|v| filter_blocked_fields(v, blocked_fields))
143                .collect();
144
145            JsonValue::Array(filtered)
146        },
147        // Non-container values pass through unchanged
148        other => other,
149    }
150}
151
152/// Find blocked fields that appear in a JSON value (output validation).
153///
154/// Unlike `filter_blocked_fields` which silently removes fields, this function
155/// identifies blocked fields without modifying the value. Used for output
156/// blocklist validation where blocked fields can be used internally but
157/// cannot appear in script output.
158///
159/// # Arguments
160///
161/// * `value` - The JSON value to check
162/// * `blocked_fields` - Set of field names that are not allowed in output
163///
164/// # Returns
165///
166/// A vector of blocked field names found in the value, along with their paths.
167pub fn find_blocked_fields_in_output(
168    value: &JsonValue,
169    blocked_fields: &HashSet<String>,
170) -> Vec<String> {
171    if blocked_fields.is_empty() {
172        return Vec::new();
173    }
174
175    let mut violations = Vec::new();
176    find_blocked_fields_recursive(value, blocked_fields, "", &mut violations);
177    violations
178}
179
180/// Recursively find blocked fields in a JSON value.
181fn find_blocked_fields_recursive(
182    value: &JsonValue,
183    blocked_fields: &HashSet<String>,
184    path: &str,
185    violations: &mut Vec<String>,
186) {
187    match value {
188        JsonValue::Object(map) => {
189            for (key, v) in map {
190                if blocked_fields.contains(key) {
191                    // Found a blocked field
192                    if path.is_empty() {
193                        violations.push(key.clone());
194                    } else {
195                        violations.push(format!("{}.{}", path, key));
196                    }
197                }
198
199                // Continue checking nested values
200                let new_path = if path.is_empty() {
201                    key.clone()
202                } else {
203                    format!("{}.{}", path, key)
204                };
205                find_blocked_fields_recursive(v, blocked_fields, &new_path, violations);
206            }
207        },
208        JsonValue::Array(arr) => {
209            for (i, v) in arr.iter().enumerate() {
210                let new_path = if path.is_empty() {
211                    format!("[{}]", i)
212                } else {
213                    format!("{}[{}]", path, i)
214                };
215                find_blocked_fields_recursive(v, blocked_fields, &new_path, violations);
216            }
217        },
218        // Non-container values don't need checking
219        _ => {},
220    }
221}
222
223/// An execution plan compiled from JavaScript AST.
224#[derive(Debug, Clone, Serialize)]
225pub struct ExecutionPlan {
226    /// The steps to execute
227    pub steps: Vec<PlanStep>,
228    /// Metadata about the plan
229    pub metadata: PlanMetadata,
230}
231
232/// Metadata about the execution plan.
233#[derive(Debug, Clone, Serialize)]
234pub struct PlanMetadata {
235    /// Total number of API calls in the plan
236    pub api_call_count: usize,
237    /// Whether any mutations (POST/PUT/DELETE/PATCH) are present
238    pub has_mutations: bool,
239    /// List of all endpoints accessed
240    pub endpoints: Vec<String>,
241    /// HTTP methods used
242    pub methods_used: Vec<String>,
243}
244
245/// A single step in the execution plan.
246#[derive(Debug, Clone, Serialize)]
247pub enum PlanStep {
248    /// API call: `const result = await api.get('/path')`
249    ApiCall {
250        result_var: String,
251        method: String,
252        path: PathTemplate,
253        body: Option<ValueExpr>,
254    },
255
256    /// Variable assignment: `const x = expr`
257    Assign { var: String, expr: ValueExpr },
258
259    /// Conditional: `if (cond) { ... } else { ... }`
260    Conditional {
261        condition: ValueExpr,
262        then_steps: Vec<PlanStep>,
263        else_steps: Vec<PlanStep>,
264    },
265
266    /// Bounded loop: `for (const x of arr.slice(0, N)) { ... }`
267    BoundedLoop {
268        item_var: String,
269        collection: ValueExpr,
270        max_iterations: usize,
271        body: Vec<PlanStep>,
272    },
273
274    /// Return statement
275    Return { value: ValueExpr },
276
277    /// Try/catch statement: `try { ... } catch (e) { ... }`
278    TryCatch {
279        try_steps: Vec<PlanStep>,
280        catch_var: Option<String>,
281        catch_steps: Vec<PlanStep>,
282        finally_steps: Vec<PlanStep>,
283    },
284
285    /// Parallel API calls: `const [a, b] = await Promise.all([api.get(...), api.get(...)])`
286    ParallelApiCalls {
287        result_var: String,
288        calls: Vec<(String, String, PathTemplate, Option<ValueExpr>)>, // (temp_var, method, path, body)
289    },
290
291    /// Continue statement: skip to next loop iteration
292    Continue,
293
294    /// Break statement: exit the current loop
295    Break,
296
297    /// MCP tool call: `const result = await mcp.call('server', 'tool', { ... })`
298    #[cfg(feature = "mcp-code-mode")]
299    McpCall {
300        result_var: String,
301        server_id: String,
302        tool_name: String,
303        args: Option<ValueExpr>,
304    },
305
306    /// SDK call: `const result = await api.getCostAndUsage({ start_date: '...' })`
307    SdkCall {
308        result_var: String,
309        operation: String,       // camelCase SDK operation name
310        args: Option<ValueExpr>, // Single object argument
311    },
312}
313
314/// A path template that may contain interpolations.
315#[derive(Debug, Clone, Serialize)]
316pub struct PathTemplate {
317    /// Parts of the path
318    pub parts: Vec<PathPart>,
319}
320
321impl PathTemplate {
322    /// Create a static path.
323    pub fn static_path(path: String) -> Self {
324        Self {
325            parts: vec![PathPart::Literal(path)],
326        }
327    }
328
329    /// Check if this path has any dynamic parts.
330    pub fn is_dynamic(&self) -> bool {
331        self.parts
332            .iter()
333            .any(|p| matches!(p, PathPart::Variable(_) | PathPart::Expression(_)))
334    }
335}
336
337/// A part of a path template.
338#[derive(Debug, Clone, Serialize)]
339pub enum PathPart {
340    /// Literal string
341    Literal(String),
342    /// Variable reference: `${id}`
343    Variable(String),
344    /// Expression: `${user.id}`
345    Expression(ValueExpr),
346}
347
348/// An expression that produces a value.
349#[derive(Debug, Clone, Serialize)]
350pub enum ValueExpr {
351    /// Literal value (null, bool, number, string)
352    Literal(JsonValue),
353
354    /// Variable reference
355    Variable(String),
356
357    /// Property access: `obj.prop`
358    PropertyAccess {
359        object: Box<ValueExpr>,
360        property: String,
361    },
362
363    /// Array index: `arr[0]`
364    ArrayIndex {
365        array: Box<ValueExpr>,
366        index: Box<ValueExpr>,
367    },
368
369    /// Object literal: `{ key: value, ...spread }`
370    ObjectLiteral { fields: Vec<ObjectField> },
371
372    /// Array literal: `[1, 2, 3]`
373    ArrayLiteral { items: Vec<ValueExpr> },
374
375    /// Array method call
376    ArrayMethod {
377        array: Box<ValueExpr>,
378        method: ArrayMethodCall,
379    },
380
381    /// Number method call: `num.toFixed()`, etc.
382    NumberMethod {
383        number: Box<ValueExpr>,
384        method: NumberMethodCall,
385    },
386
387    /// Binary operation: `a + b`, `a === b`, etc.
388    BinaryOp {
389        left: Box<ValueExpr>,
390        op: BinaryOperator,
391        right: Box<ValueExpr>,
392    },
393
394    /// Unary operation: `!a`
395    UnaryOp {
396        op: UnaryOperator,
397        operand: Box<ValueExpr>,
398    },
399
400    /// Ternary: `cond ? a : b`
401    Ternary {
402        condition: Box<ValueExpr>,
403        consequent: Box<ValueExpr>,
404        alternate: Box<ValueExpr>,
405    },
406
407    /// Optional chaining: `obj?.prop`
408    OptionalChain {
409        object: Box<ValueExpr>,
410        property: String,
411    },
412
413    /// Nullish coalescing: `a ?? b`
414    NullishCoalesce {
415        left: Box<ValueExpr>,
416        right: Box<ValueExpr>,
417    },
418
419    /// Await expression (for Promise.all, etc.)
420    Await { expr: Box<ValueExpr> },
421
422    /// Promise.all: `Promise.all([...])`
423    PromiseAll { items: Vec<ValueExpr> },
424
425    /// API call expression (when used inline, not as a statement)
426    ApiCall {
427        method: String,
428        path: PathTemplate,
429        body: Option<Box<ValueExpr>>,
430    },
431
432    /// Block expression with local variable bindings and a final result.
433    /// Used for arrow function block bodies: `x => { const a = x.foo; return a; }`
434    Block {
435        /// Local variable bindings: `[(name, expr), ...]`
436        bindings: Vec<(String, ValueExpr)>,
437        /// The final expression to evaluate and return
438        result: Box<ValueExpr>,
439    },
440
441    /// MCP tool call expression: `mcp.call('server', 'tool', args)`
442    #[cfg(feature = "mcp-code-mode")]
443    McpCall {
444        server_id: String,
445        tool_name: String,
446        args: Option<Box<ValueExpr>>,
447    },
448
449    /// SDK call expression (used in non-assignment contexts): `api.getCostAndUsage({ ... })`
450    SdkCall {
451        operation: String,
452        args: Option<Box<ValueExpr>>,
453    },
454
455    /// Built-in function call: `parseFloat(x)`, `Math.abs(x)`, `Object.keys(obj)`
456    BuiltinCall {
457        func: BuiltinFunction,
458        args: Vec<ValueExpr>,
459    },
460}
461
462/// A field within an object literal, either a regular key-value pair or a spread.
463#[derive(Debug, Clone, Serialize)]
464pub enum ObjectField {
465    /// Regular key-value pair: `key: value`
466    KeyValue { key: String, value: ValueExpr },
467    /// Spread: `...expr`
468    Spread { expr: ValueExpr },
469}
470
471/// Array method calls.
472#[derive(Debug, Clone, Serialize)]
473pub enum ArrayMethodCall {
474    /// `.map(x => expr)`
475    Map {
476        item_var: String,
477        body: Box<ValueExpr>,
478    },
479    /// `.filter(x => expr)`
480    Filter {
481        item_var: String,
482        predicate: Box<ValueExpr>,
483    },
484    /// `.find(x => expr)`
485    Find {
486        item_var: String,
487        predicate: Box<ValueExpr>,
488    },
489    /// `.slice(start, end)`
490    Slice { start: usize, end: Option<usize> },
491    /// `.length`
492    Length,
493    /// `.some(x => expr)`
494    Some {
495        item_var: String,
496        predicate: Box<ValueExpr>,
497    },
498    /// `.every(x => expr)`
499    Every {
500        item_var: String,
501        predicate: Box<ValueExpr>,
502    },
503    /// `.reduce((acc, x) => expr, init)`
504    Reduce {
505        acc_var: String,
506        item_var: String,
507        body: Box<ValueExpr>,
508        initial: Box<ValueExpr>,
509    },
510    /// `.push(item)` - returns new array (pure)
511    Push { item: Box<ValueExpr> },
512    /// `.concat(other)`
513    Concat { other: Box<ValueExpr> },
514    /// `.includes(item)`
515    Includes { item: Box<ValueExpr> },
516    /// `.indexOf(item)`
517    IndexOf { item: Box<ValueExpr> },
518    /// `.join(separator)`
519    Join { separator: Option<String> },
520    /// `.reverse()` - returns new array (pure)
521    Reverse,
522    /// `.sort()` or `.sort((a, b) => expr)` - returns new array (pure)
523    Sort {
524        comparator: Option<(String, String, Box<ValueExpr>)>,
525    },
526    /// `.flat()` - flatten nested arrays
527    Flat,
528    /// `.flatMap(x => expr)`
529    FlatMap {
530        item_var: String,
531        body: Box<ValueExpr>,
532    },
533    /// Get first element: `[0]` or `.at(0)`
534    First,
535    /// Get last element: `.at(-1)`
536    Last,
537    /// `.toLowerCase()` (string-only)
538    ToLowerCase,
539    /// `.toUpperCase()` (string-only)
540    ToUpperCase,
541    /// `.startsWith(searchString)`
542    StartsWith { search: Box<ValueExpr> },
543    /// `.endsWith(searchString)`
544    EndsWith { search: Box<ValueExpr> },
545    /// `.trim()`
546    Trim,
547    /// `.replace(search, replacement)` — first occurrence only
548    Replace {
549        search: Box<ValueExpr>,
550        replacement: Box<ValueExpr>,
551    },
552    /// `.split(separator)`
553    Split { separator: Box<ValueExpr> },
554    /// `.substring(start, end?)`
555    Substring {
556        start: Box<ValueExpr>,
557        end: Option<Box<ValueExpr>>,
558    },
559    /// `.toString()` — works on strings (identity), numbers, arrays, objects
560    ToString,
561}
562
563/// Number method calls.
564#[derive(Debug, Clone, Serialize)]
565pub enum NumberMethodCall {
566    /// `.toFixed(digits)`
567    ToFixed { digits: usize },
568    /// `.toString()`
569    ToString,
570}
571
572/// Built-in global functions and static methods.
573///
574/// These mirror JavaScript built-in functions (parseFloat, parseInt, Number)
575/// and static methods (Math.abs, Object.keys, etc.) that are commonly used
576/// in cost analysis scripts.
577#[derive(Debug, Clone, Serialize)]
578pub enum BuiltinFunction {
579    // Type conversion
580    ParseFloat,
581    ParseInt,
582    NumberCast,
583    // Math static methods
584    MathAbs,
585    MathMax,
586    MathMin,
587    MathRound,
588    MathFloor,
589    MathCeil,
590    // Object static methods
591    ObjectKeys,
592    ObjectValues,
593    ObjectEntries,
594}
595
596/// Binary operators.
597#[derive(Debug, Clone, Copy, Serialize)]
598pub enum BinaryOperator {
599    // Arithmetic
600    Add,
601    Sub,
602    Mul,
603    Div,
604    Mod,
605    // Bitwise (integer truncation)
606    BitwiseOr,
607    // Comparison
608    Eq,
609    NotEq,
610    StrictEq,
611    StrictNotEq,
612    Lt,
613    Lte,
614    Gt,
615    Gte,
616    // Logical
617    And,
618    Or,
619    // String
620    Concat,
621}
622
623/// Unary operators.
624#[derive(Debug, Clone, Copy, Serialize)]
625pub enum UnaryOperator {
626    Not,
627    Neg,
628    Plus,
629    TypeOf,
630}
631
632/// Error during plan compilation.
633#[derive(Debug, thiserror::Error)]
634pub enum CompileError {
635    #[error("Unsupported statement type: {0}")]
636    UnsupportedStatement(String),
637
638    #[error("Unsupported expression type: {0}")]
639    UnsupportedExpression(String),
640
641    #[error("Invalid API call: {0}")]
642    InvalidApiCall(String),
643
644    #[error("Unbounded loop detected")]
645    UnboundedLoop,
646
647    #[error("Invalid path template: {0}")]
648    InvalidPath(String),
649
650    #[error("Too many API calls in plan: {count} (max: {max})")]
651    TooManyApiCalls { count: usize, max: usize },
652
653    #[error("Unsupported array method: {0}")]
654    UnsupportedArrayMethod(String),
655
656    #[error("Parse error: {0}")]
657    ParseError(String),
658
659    #[error("Missing variable name")]
660    MissingVariableName,
661}
662
663/// Result of extracting an API call from an AST expression.
664///
665/// Used by `try_extract_api_call()` to return either an HTTP-mode or SDK-mode call,
666/// enabling downstream code to create the appropriate `PlanStep` or `ValueExpr`.
667enum ExtractedCall {
668    /// HTTP call: `api.get('/path', body)` → `PlanStep::ApiCall` / `ValueExpr::ApiCall`
669    Http {
670        method: String,
671        path: PathTemplate,
672        body: Option<ValueExpr>,
673    },
674    /// SDK call: `api.getCostAndUsage({ ... })` → `PlanStep::SdkCall` / `ValueExpr::SdkCall`
675    Sdk {
676        operation: String,
677        args: Option<ValueExpr>,
678    },
679}
680
681/// Compiler that converts SWC AST to ExecutionPlan.
682pub struct PlanCompiler {
683    max_api_calls: usize,
684    api_call_count: usize,
685    endpoints: Vec<String>,
686    methods_used: Vec<String>,
687    has_mutations: bool,
688    /// Set of allowed SDK operation names (camelCase). When non-empty, enables SDK mode.
689    sdk_operations: HashSet<String>,
690    /// Counter for generating unique temp variable names during destructuring.
691    destructure_counter: usize,
692}
693
694impl PlanCompiler {
695    /// Create a new compiler with default settings.
696    pub fn new() -> Self {
697        Self::with_config(&ExecutionConfig::default())
698    }
699
700    /// Create a new compiler with custom config.
701    pub fn with_config(config: &ExecutionConfig) -> Self {
702        Self {
703            max_api_calls: config.max_api_calls,
704            api_call_count: 0,
705            endpoints: Vec::new(),
706            methods_used: Vec::new(),
707            has_mutations: false,
708            sdk_operations: HashSet::new(),
709            destructure_counter: 0,
710        }
711    }
712
713    /// Set the allowed SDK operation names. When non-empty, the compiler operates in SDK mode:
714    /// `api.<operation>(args)` is compiled to `SdkCall` instead of HTTP `ApiCall`.
715    pub fn with_sdk_operations(mut self, operations: HashSet<String>) -> Self {
716        self.sdk_operations = operations;
717        self
718    }
719
720    /// Compile JavaScript code to an execution plan.
721    ///
722    /// This is a convenience method that parses the code and compiles it.
723    pub fn compile_code(&mut self, code: &str) -> Result<ExecutionPlan, CompileError> {
724        // Parse the JavaScript code using SWC
725        let cm = SourceMap::default();
726        let fm = cm.new_source_file(FileName::Anon.into(), code.to_string());
727
728        let lexer = Lexer::new(
729            Syntax::Es(Default::default()),
730            EsVersion::Es2022,
731            StringInput::from(&*fm),
732            None,
733        );
734
735        let mut parser = Parser::new_from(lexer);
736
737        let module = parser.parse_module().map_err(|e| {
738            CompileError::ParseError(format!("JavaScript parse error: {:?}", e.into_kind()))
739        })?;
740
741        // Compile the parsed module
742        self.compile(&module)
743    }
744
745    /// Compile a module to an execution plan.
746    pub fn compile(&mut self, module: &Module) -> Result<ExecutionPlan, CompileError> {
747        let mut steps = Vec::new();
748
749        for item in &module.body {
750            match item {
751                ModuleItem::Stmt(stmt) => {
752                    self.compile_statement(stmt, &mut steps)?;
753                },
754                _ => {
755                    return Err(CompileError::UnsupportedStatement(
756                        "import/export not allowed".into(),
757                    ));
758                },
759            }
760        }
761
762        // Deduplicate methods
763        self.methods_used.sort();
764        self.methods_used.dedup();
765
766        Ok(ExecutionPlan {
767            steps,
768            metadata: PlanMetadata {
769                api_call_count: self.api_call_count,
770                has_mutations: self.has_mutations,
771                endpoints: self.endpoints.clone(),
772                methods_used: self.methods_used.clone(),
773            },
774        })
775    }
776
777    fn compile_statement(
778        &mut self,
779        stmt: &Stmt,
780        steps: &mut Vec<PlanStep>,
781    ) -> Result<(), CompileError> {
782        match stmt {
783            // Variable declaration: const x = ... or const { a, b } = ...
784            Stmt::Decl(Decl::Var(var_decl)) => {
785                for decl in &var_decl.decls {
786                    if let Some(init) = &decl.init {
787                        match &decl.name {
788                            Pat::Ident(ident) => {
789                                let var_name = ident.id.sym.to_string();
790                                self.compile_var_init(&var_name, init, steps)?;
791                            },
792                            Pat::Object(obj_pat) => {
793                                self.compile_object_destructuring(obj_pat, init, steps)?;
794                            },
795                            Pat::Array(arr_pat) => {
796                                self.compile_array_destructuring(arr_pat, init, steps)?;
797                            },
798                            _ => {
799                                return Err(CompileError::UnsupportedExpression(
800                                    "complex destructuring pattern".into(),
801                                ));
802                            },
803                        }
804                    }
805                }
806            },
807
808            // Expression statement: await api.get(...), items.push(x), x = expr, etc.
809            Stmt::Expr(expr_stmt) => {
810                // Handle assignment expressions: `x = expr` or `x = await mcp.call(...)`
811                if let Expr::Assign(assign) = expr_stmt.expr.as_ref() {
812                    if assign.op == swc_ecma_ast::AssignOp::Assign {
813                        if let Some(ident) = assign.left.as_ident() {
814                            let var_name = ident.sym.to_string();
815                            self.compile_var_init(&var_name, &assign.right, steps)?;
816                            return Ok(());
817                        }
818                    }
819                }
820
821                let expr = self.compile_expr(&expr_stmt.expr)?;
822                match expr {
823                    // API calls at statement level are tracked as side effects
824                    ValueExpr::ApiCall { method, path, body } => {
825                        self.record_api_call(&method, &path);
826                        steps.push(PlanStep::ApiCall {
827                            result_var: "_".into(), // Discarded result
828                            method,
829                            path,
830                            body: body.map(|b| *b),
831                        });
832                    },
833                    // SDK calls at statement level (discarded result)
834                    ValueExpr::SdkCall { operation, args } => {
835                        steps.push(PlanStep::SdkCall {
836                            result_var: "_".into(), // Discarded result
837                            operation,
838                            args: args.map(|a| *a),
839                        });
840                    },
841                    // Mutating array methods (push, concat) as statements:
842                    // `items.push(x)` → assign the new array back to the variable
843                    ValueExpr::ArrayMethod {
844                        ref array,
845                        ref method,
846                    } if matches!(
847                        method,
848                        ArrayMethodCall::Push { .. } | ArrayMethodCall::Concat { .. }
849                    ) =>
850                    {
851                        if let ValueExpr::Variable(var_name) = array.as_ref() {
852                            steps.push(PlanStep::Assign {
853                                var: var_name.clone(),
854                                expr,
855                            });
856                        }
857                        // If array is not a simple variable (e.g., obj.arr.push(x)),
858                        // silently discard — same as before.
859                    },
860                    // Other expressions at statement level are discarded
861                    _ => {},
862                }
863            },
864
865            // If statement
866            Stmt::If(if_stmt) => {
867                let condition = self.compile_expr(&if_stmt.test)?;
868                let mut then_steps = Vec::new();
869                self.compile_statement(&if_stmt.cons, &mut then_steps)?;
870
871                let mut else_steps = Vec::new();
872                if let Some(alt) = &if_stmt.alt {
873                    self.compile_statement(alt, &mut else_steps)?;
874                }
875
876                steps.push(PlanStep::Conditional {
877                    condition,
878                    then_steps,
879                    else_steps,
880                });
881            },
882
883            // For-of statement: for (const x of arr) { ... } or for (const { a, b } of arr) { ... }
884            Stmt::ForOf(for_of) => {
885                let (item_var, destructure_bindings) = match &for_of.left {
886                    ForHead::VarDecl(decl) => {
887                        if let Some(first) = decl.decls.first() {
888                            self.extract_loop_var(&first.name)?
889                        } else {
890                            return Err(CompileError::MissingVariableName);
891                        }
892                    },
893                    ForHead::Pat(pat) => self.extract_loop_var(pat)?,
894                    _ => return Err(CompileError::MissingVariableName),
895                };
896
897                let collection = self.compile_expr(&for_of.right)?;
898
899                // Check if collection is bounded (has .slice())
900                let max_iterations = self.extract_bound(&collection).unwrap_or(100);
901
902                let mut body = destructure_bindings;
903                self.compile_statement(&for_of.body, &mut body)?;
904
905                steps.push(PlanStep::BoundedLoop {
906                    item_var,
907                    collection,
908                    max_iterations,
909                    body,
910                });
911            },
912
913            // Block statement: { ... }
914            Stmt::Block(block) => {
915                for stmt in &block.stmts {
916                    self.compile_statement(stmt, steps)?;
917                }
918            },
919
920            // Return statement
921            Stmt::Return(ret) => {
922                let value = if let Some(arg) = &ret.arg {
923                    self.compile_expr(arg)?
924                } else {
925                    ValueExpr::Literal(JsonValue::Null)
926                };
927                steps.push(PlanStep::Return { value });
928            },
929
930            // Empty statement
931            Stmt::Empty(_) => {},
932
933            // Try/catch statement
934            Stmt::Try(try_stmt) => {
935                let mut try_steps = Vec::new();
936                for stmt in &try_stmt.block.stmts {
937                    self.compile_statement(stmt, &mut try_steps)?;
938                }
939
940                let (catch_var, catch_steps) = if let Some(handler) = &try_stmt.handler {
941                    let var_name = handler.param.as_ref().map(|p| match p {
942                        swc_ecma_ast::Pat::Ident(ident) => ident.sym.to_string(),
943                        _ => "error".to_string(),
944                    });
945                    let mut catch_stmts = Vec::new();
946                    for stmt in &handler.body.stmts {
947                        self.compile_statement(stmt, &mut catch_stmts)?;
948                    }
949                    (var_name, catch_stmts)
950                } else {
951                    (None, Vec::new())
952                };
953
954                let finally_steps = if let Some(finalizer) = &try_stmt.finalizer {
955                    let mut finally_stmts = Vec::new();
956                    for stmt in &finalizer.stmts {
957                        self.compile_statement(stmt, &mut finally_stmts)?;
958                    }
959                    finally_stmts
960                } else {
961                    Vec::new()
962                };
963
964                steps.push(PlanStep::TryCatch {
965                    try_steps,
966                    catch_var,
967                    catch_steps,
968                    finally_steps,
969                });
970            },
971
972            // Continue statement: skip to next loop iteration
973            Stmt::Continue(_) => {
974                steps.push(PlanStep::Continue);
975            },
976
977            // Break statement: exit the current loop
978            Stmt::Break(_) => {
979                steps.push(PlanStep::Break);
980            },
981
982            Stmt::Decl(decl) => {
983                let msg = match decl {
984                    Decl::Fn(_) => "Function declarations are not supported. Use arrow functions inside array methods (.map, .filter) instead",
985                    Decl::Class(_) => "Class declarations are not supported",
986                    _ => "This declaration type is not supported",
987                };
988                return Err(CompileError::UnsupportedStatement(msg.into()));
989            },
990            Stmt::Switch(_) => {
991                return Err(CompileError::UnsupportedStatement(
992                    "'switch' statements are not supported. Use if/else if/else instead".into(),
993                ));
994            },
995            Stmt::Throw(_) => {
996                return Err(CompileError::UnsupportedStatement(
997                    "'throw' statements are not supported. Use try/catch for error handling".into(),
998                ));
999            },
1000            Stmt::While(_) => {
1001                return Err(CompileError::UnsupportedStatement(
1002                    "'while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1003                ));
1004            },
1005            Stmt::DoWhile(_) => {
1006                return Err(CompileError::UnsupportedStatement(
1007                    "'do-while' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1008                ));
1009            },
1010            Stmt::For(_) => {
1011                return Err(CompileError::UnsupportedStatement(
1012                    "'for(;;)' loops are not supported. Use for-of with .slice() instead: for (const item of array.slice(0, N)) { }".into(),
1013                ));
1014            },
1015            Stmt::ForIn(_) => {
1016                return Err(CompileError::UnsupportedStatement(
1017                    "'for-in' loops are not supported. Use for-of with .slice() instead".into(),
1018                ));
1019            },
1020            Stmt::Labeled(_) => {
1021                return Err(CompileError::UnsupportedStatement(
1022                    "Labeled statements are not supported".into(),
1023                ));
1024            },
1025            Stmt::With(_) => {
1026                return Err(CompileError::UnsupportedStatement(
1027                    "'with' statements are not supported".into(),
1028                ));
1029            },
1030            Stmt::Debugger(_) => {
1031                return Err(CompileError::UnsupportedStatement(
1032                    "'debugger' statements are not supported".into(),
1033                ));
1034            },
1035        }
1036
1037        Ok(())
1038    }
1039
1040    fn compile_var_init(
1041        &mut self,
1042        var_name: &str,
1043        init: &Expr,
1044        steps: &mut Vec<PlanStep>,
1045    ) -> Result<(), CompileError> {
1046        // Check if this is an await expression
1047        if let Expr::Await(await_expr) = init {
1048            // Check if awaiting an API call or SDK call
1049            if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1050                match extracted {
1051                    ExtractedCall::Http { method, path, body } => {
1052                        self.record_api_call(&method, &path);
1053                        steps.push(PlanStep::ApiCall {
1054                            result_var: var_name.into(),
1055                            method,
1056                            path,
1057                            body,
1058                        });
1059                    },
1060                    ExtractedCall::Sdk { operation, args } => {
1061                        steps.push(PlanStep::SdkCall {
1062                            result_var: var_name.into(),
1063                            operation,
1064                            args,
1065                        });
1066                    },
1067                }
1068                return Ok(());
1069            }
1070
1071            // Check if awaiting an MCP call: const x = await mcp.call(...)
1072            #[cfg(feature = "mcp-code-mode")]
1073            if let Some((server_id, tool_name, args)) =
1074                self.try_extract_mcp_call(&await_expr.arg)?
1075            {
1076                steps.push(PlanStep::McpCall {
1077                    result_var: var_name.into(),
1078                    server_id,
1079                    tool_name,
1080                    args,
1081                });
1082                return Ok(());
1083            }
1084
1085            // Check if awaiting Promise.all([api calls...])
1086            if let Expr::Call(call) = await_expr.arg.as_ref() {
1087                let inner = self.compile_call(call)?;
1088                if let ValueExpr::PromiseAll { items } = inner {
1089                    return self.compile_promise_all(var_name, items, steps);
1090                }
1091            }
1092        }
1093
1094        // Regular assignment
1095        let expr = self.compile_expr(init)?;
1096        steps.push(PlanStep::Assign {
1097            var: var_name.into(),
1098            expr,
1099        });
1100        Ok(())
1101    }
1102
1103    /// Compile `await Promise.all([api.get(...), api.get(...)])` into a ParallelApiCalls step.
1104    /// Falls back to sequential execution if any item is not an API call.
1105    fn compile_promise_all(
1106        &mut self,
1107        result_var: &str,
1108        items: Vec<ValueExpr>,
1109        steps: &mut Vec<PlanStep>,
1110    ) -> Result<(), CompileError> {
1111        let mut calls = Vec::new();
1112        let mut all_api_calls = true;
1113
1114        for (i, item) in items.iter().enumerate() {
1115            match item {
1116                ValueExpr::ApiCall { method, path, body } => {
1117                    let temp_var = format!("__promise_all_{}_{}", result_var, i);
1118                    calls.push((
1119                        temp_var,
1120                        method.clone(),
1121                        path.clone(),
1122                        body.as_ref().map(|b| *b.clone()),
1123                    ));
1124                },
1125                _ => {
1126                    all_api_calls = false;
1127                    break;
1128                },
1129            }
1130        }
1131
1132        if all_api_calls && !calls.is_empty() {
1133            // Record all API calls for metadata
1134            for (_, method, path, _) in &calls {
1135                self.record_api_call(method, path);
1136            }
1137            steps.push(PlanStep::ParallelApiCalls {
1138                result_var: result_var.into(),
1139                calls,
1140            });
1141            Ok(())
1142        } else {
1143            // Fallback: execute items sequentially as individual API calls and collect results
1144            // This handles mixed expressions in Promise.all
1145            Err(CompileError::UnsupportedExpression(
1146                "Promise.all with non-API-call expressions".into(),
1147            ))
1148        }
1149    }
1150
1151    fn compile_expr(&mut self, expr: &Expr) -> Result<ValueExpr, CompileError> {
1152        match expr {
1153            // Literal values
1154            Expr::Lit(lit) => Ok(ValueExpr::Literal(self.lit_to_json(lit))),
1155
1156            // Variable reference
1157            Expr::Ident(ident) => Ok(ValueExpr::Variable(ident.sym.to_string())),
1158
1159            // Property access: obj.prop
1160            Expr::Member(member) => {
1161                let object = Box::new(self.compile_expr(&member.obj)?);
1162
1163                // Check if this is an array method call
1164                if let MemberProp::Ident(prop) = &member.prop {
1165                    let prop_name = prop.sym.to_string();
1166                    if prop_name == "length" {
1167                        return Ok(ValueExpr::ArrayMethod {
1168                            array: object,
1169                            method: ArrayMethodCall::Length,
1170                        });
1171                    }
1172                }
1173
1174                match &member.prop {
1175                    MemberProp::Ident(ident) => Ok(ValueExpr::PropertyAccess {
1176                        object,
1177                        property: ident.sym.to_string(),
1178                    }),
1179                    MemberProp::Computed(computed) => {
1180                        let index = Box::new(self.compile_expr(&computed.expr)?);
1181                        Ok(ValueExpr::ArrayIndex {
1182                            array: object,
1183                            index,
1184                        })
1185                    }
1186                    _ => Err(CompileError::UnsupportedExpression("private property".into())),
1187                }
1188            }
1189
1190            // Call expression: fn(), api.get(), arr.map(), etc.
1191            Expr::Call(call) => self.compile_call(call),
1192
1193            // Object literal: { key: value, ...spread }
1194            Expr::Object(obj) => {
1195                let mut fields = Vec::new();
1196                for prop in &obj.props {
1197                    match prop {
1198                        PropOrSpread::Prop(prop) => {
1199                            if let Prop::KeyValue(kv) = prop.as_ref() {
1200                                let key = self.prop_name_to_string(&kv.key)?;
1201                                let value = self.compile_expr(&kv.value)?;
1202                                fields.push(ObjectField::KeyValue { key, value });
1203                            } else if let Prop::Shorthand(ident) = prop.as_ref() {
1204                                let name = ident.sym.to_string();
1205                                fields.push(ObjectField::KeyValue {
1206                                    key: name.clone(),
1207                                    value: ValueExpr::Variable(name),
1208                                });
1209                            }
1210                        }
1211                        PropOrSpread::Spread(spread) => {
1212                            let expr = self.compile_expr(&spread.expr)?;
1213                            fields.push(ObjectField::Spread { expr });
1214                        }
1215                    }
1216                }
1217                Ok(ValueExpr::ObjectLiteral { fields })
1218            }
1219
1220            // Array literal: [1, 2, 3]
1221            Expr::Array(arr) => {
1222                let mut items = Vec::new();
1223                for elem in &arr.elems {
1224                    if let Some(elem) = elem {
1225                        if elem.spread.is_some() {
1226                            return Err(CompileError::UnsupportedExpression("spread".into()));
1227                        }
1228                        items.push(self.compile_expr(&elem.expr)?);
1229                    }
1230                }
1231                Ok(ValueExpr::ArrayLiteral { items })
1232            }
1233
1234            // Template literal: `Hello ${name}`
1235            Expr::Tpl(tpl) => {
1236                // For now, treat as string concatenation
1237                // This is used primarily for path templates
1238                let mut parts = Vec::new();
1239                for (i, quasi) in tpl.quasis.iter().enumerate() {
1240                    let raw = quasi.raw.to_string();
1241                    if !raw.is_empty() {
1242                        parts.push(ValueExpr::Literal(JsonValue::String(raw)));
1243                    }
1244                    if i < tpl.exprs.len() {
1245                        parts.push(self.compile_expr(&tpl.exprs[i])?);
1246                    }
1247                }
1248
1249                // If single part, return it directly
1250                if parts.len() == 1 {
1251                    return Ok(parts.remove(0));
1252                }
1253
1254                // Otherwise, build concatenation
1255                let mut result = parts.remove(0);
1256                for part in parts {
1257                    result = ValueExpr::BinaryOp {
1258                        left: Box::new(result),
1259                        op: BinaryOperator::Concat,
1260                        right: Box::new(part),
1261                    };
1262                }
1263                Ok(result)
1264            }
1265
1266            // Binary expression: a + b, a === b
1267            Expr::Bin(bin) => {
1268                let left = Box::new(self.compile_expr(&bin.left)?);
1269                let right = Box::new(self.compile_expr(&bin.right)?);
1270                let op = self.compile_bin_op(bin.op)?;
1271                Ok(ValueExpr::BinaryOp { left, op, right })
1272            }
1273
1274            // Unary expression: !a, -a
1275            Expr::Unary(unary) => {
1276                let operand = Box::new(self.compile_expr(&unary.arg)?);
1277                let op = match unary.op {
1278                    UnaryOp::Bang => UnaryOperator::Not,
1279                    UnaryOp::Minus => UnaryOperator::Neg,
1280                    UnaryOp::TypeOf => UnaryOperator::TypeOf,
1281                    UnaryOp::Plus => UnaryOperator::Plus,
1282                    _ => return Err(CompileError::UnsupportedExpression("unary op".into())),
1283                };
1284                Ok(ValueExpr::UnaryOp { op, operand })
1285            }
1286
1287            // Conditional/ternary: cond ? a : b
1288            Expr::Cond(cond) => {
1289                let condition = Box::new(self.compile_expr(&cond.test)?);
1290                let consequent = Box::new(self.compile_expr(&cond.cons)?);
1291                let alternate = Box::new(self.compile_expr(&cond.alt)?);
1292                Ok(ValueExpr::Ternary {
1293                    condition,
1294                    consequent,
1295                    alternate,
1296                })
1297            }
1298
1299            // Await expression
1300            Expr::Await(await_expr) => {
1301                // Check if it's an API call or SDK call
1302                if let Some(extracted) = self.try_extract_api_call(&await_expr.arg)? {
1303                    return match extracted {
1304                        ExtractedCall::Http { method, path, body } => {
1305                            self.record_api_call(&method, &path);
1306                            Ok(ValueExpr::ApiCall {
1307                                method,
1308                                path,
1309                                body: body.map(Box::new),
1310                            })
1311                        }
1312                        ExtractedCall::Sdk { operation, args } => {
1313                            Ok(ValueExpr::SdkCall {
1314                                operation,
1315                                args: args.map(Box::new),
1316                            })
1317                        }
1318                    };
1319                }
1320                // Check if it's an MCP call
1321                #[cfg(feature = "mcp-code-mode")]
1322                if let Some((server_id, tool_name, args)) = self.try_extract_mcp_call(&await_expr.arg)? {
1323                    return Ok(ValueExpr::McpCall {
1324                        server_id,
1325                        tool_name,
1326                        args: args.map(Box::new),
1327                    });
1328                }
1329                // Otherwise, just await the inner expression
1330                let inner = self.compile_expr(&await_expr.arg)?;
1331                Ok(ValueExpr::Await {
1332                    expr: Box::new(inner),
1333                })
1334            }
1335
1336            // Arrow function (for use in .map(), .filter(), etc.)
1337            Expr::Arrow(_) => {
1338                // Arrow functions are handled specially in array method compilation
1339                Err(CompileError::UnsupportedExpression(
1340                    "arrow function outside array method".into(),
1341                ))
1342            }
1343
1344            // Parenthesized expression
1345            Expr::Paren(paren) => self.compile_expr(&paren.expr),
1346
1347            // Optional chaining: obj?.prop
1348            Expr::OptChain(opt) => {
1349                match opt.base.as_ref() {
1350                    OptChainBase::Member(member) => {
1351                        let object = Box::new(self.compile_expr(&member.obj)?);
1352                        if let MemberProp::Ident(ident) = &member.prop {
1353                            Ok(ValueExpr::OptionalChain {
1354                                object,
1355                                property: ident.sym.to_string(),
1356                            })
1357                        } else {
1358                            Err(CompileError::UnsupportedExpression("computed optional chain".into()))
1359                        }
1360                    }
1361                    _ => Err(CompileError::UnsupportedExpression("optional call".into())),
1362                }
1363            }
1364
1365            Expr::This(_) => Err(CompileError::UnsupportedExpression(
1366                "'this' keyword is not supported".into(),
1367            )),
1368            Expr::Fn(_) => Err(CompileError::UnsupportedExpression(
1369                "Function expressions are not supported. Use arrow functions inside array methods (.map, .filter) instead".into(),
1370            )),
1371            Expr::Update(_) => Err(CompileError::UnsupportedExpression(
1372                "Increment/decrement operators (++, --) are not supported. Use 'x = x + 1' instead".into(),
1373            )),
1374            Expr::New(_) => Err(CompileError::UnsupportedExpression(
1375                "'new' keyword is not supported".into(),
1376            )),
1377            Expr::Seq(_) => Err(CompileError::UnsupportedExpression(
1378                "Sequence expressions (comma operator) are not supported. Use separate statements instead".into(),
1379            )),
1380            Expr::TaggedTpl(_) => Err(CompileError::UnsupportedExpression(
1381                "Tagged template literals are not supported. Use regular template literals instead".into(),
1382            )),
1383            Expr::Class(_) => Err(CompileError::UnsupportedExpression(
1384                "Class expressions are not supported".into(),
1385            )),
1386            Expr::Yield(_) => Err(CompileError::UnsupportedExpression(
1387                "Generator yield is not supported".into(),
1388            )),
1389            Expr::SuperProp(_) => Err(CompileError::UnsupportedExpression(
1390                "'super' is not supported".into(),
1391            )),
1392            Expr::Assign(_) => Err(CompileError::UnsupportedExpression(
1393                "Assignment expressions are not supported here. Use a separate variable declaration instead".into(),
1394            )),
1395            _ => Err(CompileError::UnsupportedExpression(
1396                "This expression type is not supported in the JavaScript subset".into(),
1397            )),
1398        }
1399    }
1400
1401    fn compile_call(&mut self, call: &CallExpr) -> Result<ValueExpr, CompileError> {
1402        // Check if this is an API call (HTTP) or SDK call
1403        if let Some(extracted) = self.try_extract_api_call(&Expr::Call(call.clone()))? {
1404            return match extracted {
1405                ExtractedCall::Http { method, path, body } => {
1406                    self.record_api_call(&method, &path);
1407                    Ok(ValueExpr::ApiCall {
1408                        method,
1409                        path,
1410                        body: body.map(Box::new),
1411                    })
1412                },
1413                ExtractedCall::Sdk { operation, args } => Ok(ValueExpr::SdkCall {
1414                    operation,
1415                    args: args.map(Box::new),
1416                }),
1417            };
1418        }
1419
1420        // Check if this is an MCP call: mcp.call('server', 'tool', args)
1421        #[cfg(feature = "mcp-code-mode")]
1422        if let Some((server_id, tool_name, args)) =
1423            self.try_extract_mcp_call(&Expr::Call(call.clone()))?
1424        {
1425            return Ok(ValueExpr::McpCall {
1426                server_id,
1427                tool_name,
1428                args: args.map(Box::new),
1429            });
1430        }
1431
1432        // Check if this is Promise.all
1433        if let Callee::Expr(callee) = &call.callee {
1434            if let Expr::Member(member) = callee.as_ref() {
1435                if let Expr::Ident(obj) = member.obj.as_ref() {
1436                    if obj.sym.as_ref() == "Promise" {
1437                        if let MemberProp::Ident(prop) = &member.prop {
1438                            if prop.sym.as_ref() == "all" {
1439                                if let Some(arg) = call.args.first() {
1440                                    if let Expr::Array(arr) = arg.expr.as_ref() {
1441                                        let mut items = Vec::new();
1442                                        for elem in &arr.elems {
1443                                            if let Some(elem) = elem {
1444                                                items.push(self.compile_expr(&elem.expr)?);
1445                                            }
1446                                        }
1447                                        return Ok(ValueExpr::PromiseAll { items });
1448                                    }
1449                                }
1450                            }
1451                        }
1452                    }
1453                }
1454            }
1455        }
1456
1457        // Check if this is an array method: arr.map(), arr.filter(), etc.
1458        if let Callee::Expr(callee) = &call.callee {
1459            if let Expr::Member(member) = callee.as_ref() {
1460                let array = Box::new(self.compile_expr(&member.obj)?);
1461
1462                if let MemberProp::Ident(method_ident) = &member.prop {
1463                    let method_name = method_ident.sym.as_ref();
1464
1465                    match method_name {
1466                        "map" => {
1467                            let (item_var, body) = self.extract_arrow_callback(call)?;
1468                            return Ok(ValueExpr::ArrayMethod {
1469                                array,
1470                                method: ArrayMethodCall::Map {
1471                                    item_var,
1472                                    body: Box::new(body),
1473                                },
1474                            });
1475                        },
1476                        "filter" => {
1477                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1478                            return Ok(ValueExpr::ArrayMethod {
1479                                array,
1480                                method: ArrayMethodCall::Filter {
1481                                    item_var,
1482                                    predicate: Box::new(predicate),
1483                                },
1484                            });
1485                        },
1486                        "find" => {
1487                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1488                            return Ok(ValueExpr::ArrayMethod {
1489                                array,
1490                                method: ArrayMethodCall::Find {
1491                                    item_var,
1492                                    predicate: Box::new(predicate),
1493                                },
1494                            });
1495                        },
1496                        "some" => {
1497                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1498                            return Ok(ValueExpr::ArrayMethod {
1499                                array,
1500                                method: ArrayMethodCall::Some {
1501                                    item_var,
1502                                    predicate: Box::new(predicate),
1503                                },
1504                            });
1505                        },
1506                        "every" => {
1507                            let (item_var, predicate) = self.extract_arrow_callback(call)?;
1508                            return Ok(ValueExpr::ArrayMethod {
1509                                array,
1510                                method: ArrayMethodCall::Every {
1511                                    item_var,
1512                                    predicate: Box::new(predicate),
1513                                },
1514                            });
1515                        },
1516                        "flatMap" => {
1517                            let (item_var, body) = self.extract_arrow_callback(call)?;
1518                            return Ok(ValueExpr::ArrayMethod {
1519                                array,
1520                                method: ArrayMethodCall::FlatMap {
1521                                    item_var,
1522                                    body: Box::new(body),
1523                                },
1524                            });
1525                        },
1526                        "slice" => {
1527                            let start = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1528                            let end = self.extract_number_arg(call, 1)?.map(|n| n as usize);
1529                            return Ok(ValueExpr::ArrayMethod {
1530                                array,
1531                                method: ArrayMethodCall::Slice { start, end },
1532                            });
1533                        },
1534                        "push" => {
1535                            if let Some(arg) = call.args.first() {
1536                                let item = Box::new(self.compile_expr(&arg.expr)?);
1537                                return Ok(ValueExpr::ArrayMethod {
1538                                    array,
1539                                    method: ArrayMethodCall::Push { item },
1540                                });
1541                            }
1542                        },
1543                        "concat" => {
1544                            if let Some(arg) = call.args.first() {
1545                                let other = Box::new(self.compile_expr(&arg.expr)?);
1546                                return Ok(ValueExpr::ArrayMethod {
1547                                    array,
1548                                    method: ArrayMethodCall::Concat { other },
1549                                });
1550                            }
1551                        },
1552                        "includes" => {
1553                            if let Some(arg) = call.args.first() {
1554                                let item = Box::new(self.compile_expr(&arg.expr)?);
1555                                return Ok(ValueExpr::ArrayMethod {
1556                                    array,
1557                                    method: ArrayMethodCall::Includes { item },
1558                                });
1559                            }
1560                        },
1561                        "indexOf" => {
1562                            if let Some(arg) = call.args.first() {
1563                                let item = Box::new(self.compile_expr(&arg.expr)?);
1564                                return Ok(ValueExpr::ArrayMethod {
1565                                    array,
1566                                    method: ArrayMethodCall::IndexOf { item },
1567                                });
1568                            }
1569                        },
1570                        "join" => {
1571                            let separator = if let Some(arg) = call.args.first() {
1572                                if let Expr::Lit(Lit::Str(s)) = arg.expr.as_ref() {
1573                                    Some(s.value.to_string_lossy().into_owned())
1574                                } else {
1575                                    None
1576                                }
1577                            } else {
1578                                None
1579                            };
1580                            return Ok(ValueExpr::ArrayMethod {
1581                                array,
1582                                method: ArrayMethodCall::Join { separator },
1583                            });
1584                        },
1585                        "reverse" => {
1586                            return Ok(ValueExpr::ArrayMethod {
1587                                array,
1588                                method: ArrayMethodCall::Reverse,
1589                            });
1590                        },
1591                        "sort" => {
1592                            let comparator = if !call.args.is_empty() {
1593                                let (a_var, b_var, body) = self.extract_reduce_callback(call)?;
1594                                Some((a_var, b_var, Box::new(body)))
1595                            } else {
1596                                None
1597                            };
1598                            return Ok(ValueExpr::ArrayMethod {
1599                                array,
1600                                method: ArrayMethodCall::Sort { comparator },
1601                            });
1602                        },
1603                        "flat" => {
1604                            return Ok(ValueExpr::ArrayMethod {
1605                                array,
1606                                method: ArrayMethodCall::Flat,
1607                            });
1608                        },
1609                        "at" => {
1610                            if let Some(n) = self.extract_number_arg(call, 0)? {
1611                                if n == 0 {
1612                                    return Ok(ValueExpr::ArrayMethod {
1613                                        array,
1614                                        method: ArrayMethodCall::First,
1615                                    });
1616                                } else if n == -1 {
1617                                    return Ok(ValueExpr::ArrayMethod {
1618                                        array,
1619                                        method: ArrayMethodCall::Last,
1620                                    });
1621                                }
1622                            }
1623                        },
1624                        "reduce" => {
1625                            // reduce((acc, item) => expr, initialValue)
1626                            if call.args.len() >= 2 {
1627                                let (acc_var, item_var, body) =
1628                                    self.extract_reduce_callback(call)?;
1629                                let initial = Box::new(self.compile_expr(&call.args[1].expr)?);
1630                                return Ok(ValueExpr::ArrayMethod {
1631                                    array,
1632                                    method: ArrayMethodCall::Reduce {
1633                                        acc_var,
1634                                        item_var,
1635                                        body: Box::new(body),
1636                                        initial,
1637                                    },
1638                                });
1639                            }
1640                        },
1641                        "toFixed" => {
1642                            // Number.toFixed(digits) - treat as a number method
1643                            let digits = self.extract_number_arg(call, 0)?.unwrap_or(0) as usize;
1644                            return Ok(ValueExpr::NumberMethod {
1645                                number: array, // The "array" here is actually the number
1646                                method: NumberMethodCall::ToFixed { digits },
1647                            });
1648                        },
1649                        "toLowerCase" => {
1650                            return Ok(ValueExpr::ArrayMethod {
1651                                array,
1652                                method: ArrayMethodCall::ToLowerCase,
1653                            });
1654                        },
1655                        "toUpperCase" => {
1656                            return Ok(ValueExpr::ArrayMethod {
1657                                array,
1658                                method: ArrayMethodCall::ToUpperCase,
1659                            });
1660                        },
1661                        "startsWith" => {
1662                            let arg = call.args.first().ok_or_else(|| {
1663                                CompileError::UnsupportedExpression(
1664                                    "startsWith() requires a search argument".into(),
1665                                )
1666                            })?;
1667                            let search = Box::new(self.compile_expr(&arg.expr)?);
1668                            return Ok(ValueExpr::ArrayMethod {
1669                                array,
1670                                method: ArrayMethodCall::StartsWith { search },
1671                            });
1672                        },
1673                        "endsWith" => {
1674                            let arg = call.args.first().ok_or_else(|| {
1675                                CompileError::UnsupportedExpression(
1676                                    "endsWith() requires a search argument".into(),
1677                                )
1678                            })?;
1679                            let search = Box::new(self.compile_expr(&arg.expr)?);
1680                            return Ok(ValueExpr::ArrayMethod {
1681                                array,
1682                                method: ArrayMethodCall::EndsWith { search },
1683                            });
1684                        },
1685                        "trim" => {
1686                            return Ok(ValueExpr::ArrayMethod {
1687                                array,
1688                                method: ArrayMethodCall::Trim,
1689                            });
1690                        },
1691                        "replace" => {
1692                            if call.args.len() < 2 {
1693                                return Err(CompileError::UnsupportedExpression(
1694                                    "replace() requires search and replacement arguments".into(),
1695                                ));
1696                            }
1697                            let search = Box::new(self.compile_expr(&call.args[0].expr)?);
1698                            let replacement = Box::new(self.compile_expr(&call.args[1].expr)?);
1699                            return Ok(ValueExpr::ArrayMethod {
1700                                array,
1701                                method: ArrayMethodCall::Replace {
1702                                    search,
1703                                    replacement,
1704                                },
1705                            });
1706                        },
1707                        "split" => {
1708                            let arg = call.args.first().ok_or_else(|| {
1709                                CompileError::UnsupportedExpression(
1710                                    "split() requires a separator argument".into(),
1711                                )
1712                            })?;
1713                            let separator = Box::new(self.compile_expr(&arg.expr)?);
1714                            return Ok(ValueExpr::ArrayMethod {
1715                                array,
1716                                method: ArrayMethodCall::Split { separator },
1717                            });
1718                        },
1719                        "substring" => {
1720                            let arg = call.args.first().ok_or_else(|| {
1721                                CompileError::UnsupportedExpression(
1722                                    "substring() requires a start argument".into(),
1723                                )
1724                            })?;
1725                            let start = Box::new(self.compile_expr(&arg.expr)?);
1726                            let end = if call.args.len() >= 2 {
1727                                Some(Box::new(self.compile_expr(&call.args[1].expr)?))
1728                            } else {
1729                                None
1730                            };
1731                            return Ok(ValueExpr::ArrayMethod {
1732                                array,
1733                                method: ArrayMethodCall::Substring { start, end },
1734                            });
1735                        },
1736                        "toString" => {
1737                            return Ok(ValueExpr::ArrayMethod {
1738                                array,
1739                                method: ArrayMethodCall::ToString,
1740                            });
1741                        },
1742                        _ => {},
1743                    }
1744                }
1745            }
1746        }
1747
1748        // Check for built-in global functions: parseFloat(), parseInt(), Number()
1749        if let Callee::Expr(callee) = &call.callee {
1750            if let Expr::Ident(ident) = callee.as_ref() {
1751                let func = match ident.sym.as_ref() {
1752                    "parseFloat" => Some(BuiltinFunction::ParseFloat),
1753                    "parseInt" => Some(BuiltinFunction::ParseInt),
1754                    "Number" => Some(BuiltinFunction::NumberCast),
1755                    _ => None,
1756                };
1757                if let Some(func) = func {
1758                    let args = call
1759                        .args
1760                        .iter()
1761                        .map(|a| self.compile_expr(&a.expr))
1762                        .collect::<Result<Vec<_>, _>>()?;
1763                    return Ok(ValueExpr::BuiltinCall { func, args });
1764                }
1765            }
1766
1767            // Check for static method calls: Math.abs(), Object.keys(), etc.
1768            if let Expr::Member(member) = callee.as_ref() {
1769                if let Expr::Ident(obj) = member.obj.as_ref() {
1770                    if let MemberProp::Ident(prop) = &member.prop {
1771                        let func = match (obj.sym.as_ref(), prop.sym.as_ref()) {
1772                            ("Math", "abs") => Some(BuiltinFunction::MathAbs),
1773                            ("Math", "max") => Some(BuiltinFunction::MathMax),
1774                            ("Math", "min") => Some(BuiltinFunction::MathMin),
1775                            ("Math", "round") => Some(BuiltinFunction::MathRound),
1776                            ("Math", "floor") => Some(BuiltinFunction::MathFloor),
1777                            ("Math", "ceil") => Some(BuiltinFunction::MathCeil),
1778                            ("Object", "keys") => Some(BuiltinFunction::ObjectKeys),
1779                            ("Object", "values") => Some(BuiltinFunction::ObjectValues),
1780                            ("Object", "entries") => Some(BuiltinFunction::ObjectEntries),
1781                            _ => None,
1782                        };
1783                        if let Some(func) = func {
1784                            let args = call
1785                                .args
1786                                .iter()
1787                                .map(|a| self.compile_expr(&a.expr))
1788                                .collect::<Result<Vec<_>, _>>()?;
1789                            return Ok(ValueExpr::BuiltinCall { func, args });
1790                        }
1791                    }
1792                }
1793            }
1794        }
1795
1796        Err(CompileError::UnsupportedExpression("function call".into()))
1797    }
1798
1799    fn try_extract_api_call(&mut self, expr: &Expr) -> Result<Option<ExtractedCall>, CompileError> {
1800        let call = match expr {
1801            Expr::Call(c) => c,
1802            _ => return Ok(None),
1803        };
1804
1805        if let Callee::Expr(callee) = &call.callee {
1806            if let Expr::Member(member) = callee.as_ref() {
1807                if let Expr::Ident(obj) = member.obj.as_ref() {
1808                    if obj.sym.as_ref() == "api" {
1809                        if let MemberProp::Ident(method_ident) = &member.prop {
1810                            let method_name = method_ident.sym.as_ref();
1811
1812                            if !self.sdk_operations.is_empty() {
1813                                // SDK mode: validate against allowed operation names
1814                                if !self.sdk_operations.contains(method_name) {
1815                                    return Err(CompileError::InvalidApiCall(format!(
1816                                        "Unknown SDK operation: api.{}(). Check the code mode schema resource for available operations.",
1817                                        method_name
1818                                    )));
1819                                }
1820                                let args = if let Some(arg) = call.args.first() {
1821                                    Some(self.compile_expr(&arg.expr)?)
1822                                } else {
1823                                    None
1824                                };
1825                                self.api_call_count += 1;
1826                                let op_endpoint = format!("sdk:{}", method_name);
1827                                if !self.endpoints.contains(&op_endpoint) {
1828                                    self.endpoints.push(op_endpoint);
1829                                }
1830                                if !self.methods_used.contains(&method_name.to_string()) {
1831                                    self.methods_used.push(method_name.to_string());
1832                                }
1833                                return Ok(Some(ExtractedCall::Sdk {
1834                                    operation: method_name.to_string(),
1835                                    args,
1836                                }));
1837                            }
1838
1839                            // HTTP mode: validate it's a known HTTP method
1840                            if HttpMethod::from_str(method_name).is_none() {
1841                                return Err(CompileError::InvalidApiCall(format!(
1842                                    "Unknown method: api.{}",
1843                                    method_name
1844                                )));
1845                            }
1846
1847                            // Extract path from first argument
1848                            let path = if let Some(arg) = call.args.first() {
1849                                self.extract_path_template(&arg.expr)?
1850                            } else {
1851                                return Err(CompileError::InvalidApiCall(
1852                                    "API call requires path".into(),
1853                                ));
1854                            };
1855
1856                            // Extract body from second argument (for POST, PUT, PATCH)
1857                            let body = if let Some(arg) = call.args.get(1) {
1858                                Some(self.compile_expr(&arg.expr)?)
1859                            } else {
1860                                None
1861                            };
1862
1863                            return Ok(Some(ExtractedCall::Http {
1864                                method: method_name.to_uppercase(),
1865                                path,
1866                                body,
1867                            }));
1868                        }
1869                    }
1870                }
1871            }
1872        }
1873
1874        Ok(None)
1875    }
1876
1877    /// Try to extract an MCP call: `mcp.call('server', 'tool', { args })`
1878    ///
1879    /// Returns `(server_id, tool_name, args)` if the expression is an MCP call.
1880    #[cfg(feature = "mcp-code-mode")]
1881    fn try_extract_mcp_call(
1882        &mut self,
1883        expr: &Expr,
1884    ) -> Result<Option<(String, String, Option<ValueExpr>)>, CompileError> {
1885        let call = match expr {
1886            Expr::Call(c) => c,
1887            _ => return Ok(None),
1888        };
1889
1890        if let Callee::Expr(callee) = &call.callee {
1891            if let Expr::Member(member) = callee.as_ref() {
1892                if let Expr::Ident(obj) = member.obj.as_ref() {
1893                    if obj.sym.as_ref() == "mcp" {
1894                        if let MemberProp::Ident(method_ident) = &member.prop {
1895                            if method_ident.sym.as_ref() == "call" {
1896                                // Extract server_id from first arg (string literal)
1897                                let server_id = call.args.first()
1898                                    .and_then(|a| {
1899                                        if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1900                                            Some(s.value.to_string_lossy().into_owned())
1901                                        } else {
1902                                            None
1903                                        }
1904                                    })
1905                                    .ok_or_else(|| CompileError::UnsupportedExpression(
1906                                        "mcp.call() first argument must be a string literal (server_id)".into(),
1907                                    ))?;
1908
1909                                // Extract tool_name from second arg (string literal)
1910                                let tool_name = call.args.get(1)
1911                                    .and_then(|a| {
1912                                        if let Expr::Lit(Lit::Str(s)) = a.expr.as_ref() {
1913                                            Some(s.value.to_string_lossy().into_owned())
1914                                        } else {
1915                                            None
1916                                        }
1917                                    })
1918                                    .ok_or_else(|| CompileError::UnsupportedExpression(
1919                                        "mcp.call() second argument must be a string literal (tool_name)".into(),
1920                                    ))?;
1921
1922                                // Extract args from third arg (optional object expression)
1923                                let args = call
1924                                    .args
1925                                    .get(2)
1926                                    .map(|a| self.compile_expr(&a.expr))
1927                                    .transpose()?;
1928
1929                                return Ok(Some((server_id, tool_name, args)));
1930                            }
1931                        }
1932                    }
1933                }
1934            }
1935        }
1936
1937        Ok(None)
1938    }
1939
1940    fn extract_path_template(&mut self, expr: &Expr) -> Result<PathTemplate, CompileError> {
1941        match expr {
1942            // Simple string: '/users'
1943            Expr::Lit(Lit::Str(s)) => Ok(PathTemplate::static_path(
1944                s.value.to_string_lossy().into_owned(),
1945            )),
1946
1947            // Template literal: `/users/${id}`
1948            Expr::Tpl(tpl) => {
1949                let mut parts = Vec::new();
1950                for (i, quasi) in tpl.quasis.iter().enumerate() {
1951                    let raw = quasi.raw.to_string();
1952                    if !raw.is_empty() {
1953                        parts.push(PathPart::Literal(raw));
1954                    }
1955                    if i < tpl.exprs.len() {
1956                        // Check if it's a simple variable
1957                        if let Expr::Ident(ident) = tpl.exprs[i].as_ref() {
1958                            parts.push(PathPart::Variable(ident.sym.to_string()));
1959                        } else {
1960                            // Complex expression
1961                            let expr = self.compile_expr(&tpl.exprs[i])?;
1962                            parts.push(PathPart::Expression(expr));
1963                        }
1964                    }
1965                }
1966                Ok(PathTemplate { parts })
1967            },
1968
1969            _ => Err(CompileError::InvalidPath(
1970                "Path must be a string or template literal".into(),
1971            )),
1972        }
1973    }
1974
1975    fn extract_arrow_callback(
1976        &mut self,
1977        call: &CallExpr,
1978    ) -> Result<(String, ValueExpr), CompileError> {
1979        let arg = call
1980            .args
1981            .first()
1982            .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
1983
1984        if let Expr::Arrow(arrow) = arg.expr.as_ref() {
1985            // Get parameter name
1986            let param_name = if let Some(Pat::Ident(ident)) = arrow.params.first() {
1987                ident.id.sym.to_string()
1988            } else {
1989                return Err(CompileError::UnsupportedExpression(
1990                    "complex callback parameter".into(),
1991                ));
1992            };
1993
1994            // Compile body
1995            let body = match &*arrow.body {
1996                BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
1997                BlockStmtOrExpr::BlockStmt(block) => {
1998                    // For block bodies, collect variable bindings and find return statement
1999                    let mut bindings: Vec<(String, ValueExpr)> = Vec::new();
2000                    let mut return_expr: Option<ValueExpr> = None;
2001
2002                    for stmt in &block.stmts {
2003                        match stmt {
2004                            // Variable declaration: const x = ...; or let x = ...;
2005                            Stmt::Decl(Decl::Var(var_decl)) => {
2006                                for decl in &var_decl.decls {
2007                                    let var_name = self.get_var_name(&decl.name)?;
2008                                    if let Some(init) = &decl.init {
2009                                        let expr = self.compile_expr(init)?;
2010                                        bindings.push((var_name, expr));
2011                                    }
2012                                }
2013                            },
2014                            // Return statement
2015                            Stmt::Return(ret) => {
2016                                if let Some(arg) = &ret.arg {
2017                                    return_expr = Some(self.compile_expr(arg)?);
2018                                }
2019                                break; // Stop processing after return
2020                            },
2021                            // Expression statement (e.g., a side effect)
2022                            Stmt::Expr(_) => {
2023                                // Ignore expression statements in arrow body for now
2024                            },
2025                            _ => {},
2026                        }
2027                    }
2028
2029                    match return_expr {
2030                        Some(result) if bindings.is_empty() => result,
2031                        Some(result) => ValueExpr::Block {
2032                            bindings,
2033                            result: Box::new(result),
2034                        },
2035                        None => {
2036                            return Err(CompileError::UnsupportedExpression(
2037                                "callback block without return".into(),
2038                            ));
2039                        },
2040                    }
2041                },
2042            };
2043
2044            Ok((param_name, body))
2045        } else {
2046            Err(CompileError::UnsupportedExpression(
2047                "callback must be arrow function".into(),
2048            ))
2049        }
2050    }
2051
2052    /// Extract reduce callback: (acc, item) => expr
2053    fn extract_reduce_callback(
2054        &mut self,
2055        call: &CallExpr,
2056    ) -> Result<(String, String, ValueExpr), CompileError> {
2057        let arg = call
2058            .args
2059            .first()
2060            .ok_or_else(|| CompileError::UnsupportedExpression("missing callback".into()))?;
2061
2062        if let Expr::Arrow(arrow) = arg.expr.as_ref() {
2063            // Reduce callback should have 2 parameters: (acc, item)
2064            if arrow.params.len() < 2 {
2065                return Err(CompileError::UnsupportedExpression(
2066                    "reduce callback must have 2 parameters".into(),
2067                ));
2068            }
2069
2070            let acc_name = if let Pat::Ident(ident) = &arrow.params[0] {
2071                ident.id.sym.to_string()
2072            } else {
2073                return Err(CompileError::UnsupportedExpression(
2074                    "complex callback parameter".into(),
2075                ));
2076            };
2077
2078            let item_name = if let Pat::Ident(ident) = &arrow.params[1] {
2079                ident.id.sym.to_string()
2080            } else {
2081                return Err(CompileError::UnsupportedExpression(
2082                    "complex callback parameter".into(),
2083                ));
2084            };
2085
2086            // Compile body
2087            let body = match &*arrow.body {
2088                BlockStmtOrExpr::Expr(expr) => self.compile_expr(expr)?,
2089                BlockStmtOrExpr::BlockStmt(block) => {
2090                    // For block bodies, look for return statement
2091                    for stmt in &block.stmts {
2092                        if let Stmt::Return(ret) = stmt {
2093                            if let Some(arg) = &ret.arg {
2094                                return Ok((acc_name, item_name, self.compile_expr(arg)?));
2095                            }
2096                        }
2097                    }
2098                    return Err(CompileError::UnsupportedExpression(
2099                        "callback block without return".into(),
2100                    ));
2101                },
2102            };
2103
2104            Ok((acc_name, item_name, body))
2105        } else {
2106            Err(CompileError::UnsupportedExpression(
2107                "callback must be arrow function".into(),
2108            ))
2109        }
2110    }
2111
2112    fn extract_number_arg(
2113        &self,
2114        call: &CallExpr,
2115        index: usize,
2116    ) -> Result<Option<i64>, CompileError> {
2117        if let Some(arg) = call.args.get(index) {
2118            if let Expr::Lit(Lit::Num(n)) = arg.expr.as_ref() {
2119                return Ok(Some(n.value as i64));
2120            }
2121            if let Expr::Unary(unary) = arg.expr.as_ref() {
2122                if unary.op == UnaryOp::Minus {
2123                    if let Expr::Lit(Lit::Num(n)) = unary.arg.as_ref() {
2124                        return Ok(Some(-(n.value as i64)));
2125                    }
2126                }
2127            }
2128        }
2129        Ok(None)
2130    }
2131
2132    fn extract_bound(&self, expr: &ValueExpr) -> Option<usize> {
2133        if let ValueExpr::ArrayMethod { method, .. } = expr {
2134            if let ArrayMethodCall::Slice { end, .. } = method {
2135                return *end;
2136            }
2137        }
2138        None
2139    }
2140
2141    fn get_var_name(&self, pat: &Pat) -> Result<String, CompileError> {
2142        match pat {
2143            Pat::Ident(ident) => Ok(ident.id.sym.to_string()),
2144            _ => Err(CompileError::UnsupportedExpression(
2145                "complex destructuring".into(),
2146            )),
2147        }
2148    }
2149
2150    /// Generate a unique temp variable name for destructuring.
2151    fn next_temp_var(&mut self) -> String {
2152        let name = format!("__destructure_{}", self.destructure_counter);
2153        self.destructure_counter += 1;
2154        name
2155    }
2156
2157    /// Extract loop variable and destructuring steps for for-of loops.
2158    /// Returns (item_var, steps_to_prepend_to_body).
2159    fn extract_loop_var(&mut self, pat: &Pat) -> Result<(String, Vec<PlanStep>), CompileError> {
2160        match pat {
2161            Pat::Ident(ident) => Ok((ident.id.sym.to_string(), Vec::new())),
2162            Pat::Object(obj_pat) => {
2163                let temp_var = self.next_temp_var();
2164                let bindings = Self::extract_object_bindings(obj_pat)?;
2165                let steps = bindings
2166                    .into_iter()
2167                    .map(|(var_name, property)| PlanStep::Assign {
2168                        var: var_name,
2169                        expr: ValueExpr::PropertyAccess {
2170                            object: Box::new(ValueExpr::Variable(temp_var.clone())),
2171                            property,
2172                        },
2173                    })
2174                    .collect();
2175                Ok((temp_var, steps))
2176            },
2177            Pat::Array(arr_pat) => {
2178                let temp_var = self.next_temp_var();
2179                let mut steps = Vec::new();
2180                for (i, elem) in arr_pat.elems.iter().enumerate() {
2181                    if let Some(p) = elem {
2182                        let var_name = self.get_var_name(p)?;
2183                        steps.push(PlanStep::Assign {
2184                            var: var_name,
2185                            expr: ValueExpr::ArrayIndex {
2186                                array: Box::new(ValueExpr::Variable(temp_var.clone())),
2187                                index: Box::new(ValueExpr::Literal(JsonValue::Number(
2188                                    (i as i64).into(),
2189                                ))),
2190                            },
2191                        });
2192                    }
2193                }
2194                Ok((temp_var, steps))
2195            },
2196            _ => Err(CompileError::UnsupportedExpression(
2197                "complex loop variable pattern".into(),
2198            )),
2199        }
2200    }
2201
2202    /// Extract (var_name, property_key) bindings from an object destructuring pattern.
2203    /// `{ a, b }` → [("a", "a"), ("b", "b")]
2204    /// `{ id: userId }` → [("userId", "id")]
2205    fn extract_object_bindings(obj_pat: &ObjectPat) -> Result<Vec<(String, String)>, CompileError> {
2206        let mut bindings = Vec::new();
2207        for prop in &obj_pat.props {
2208            match prop {
2209                ObjectPatProp::Assign(assign) => {
2210                    // Shorthand: `{ x }` — reject `{ x = default }` explicitly
2211                    if assign.value.is_some() {
2212                        return Err(CompileError::UnsupportedExpression(
2213                            "default values in destructuring".into(),
2214                        ));
2215                    }
2216                    let name = assign.key.sym.to_string();
2217                    bindings.push((name.clone(), name));
2218                },
2219                ObjectPatProp::KeyValue(kv) => {
2220                    // Renamed: `{ id: userId }`
2221                    let key = match &kv.key {
2222                        PropName::Ident(ident) => ident.sym.to_string(),
2223                        PropName::Str(s) => s.value.to_string_lossy().into_owned(),
2224                        _ => {
2225                            return Err(CompileError::UnsupportedExpression(
2226                                "computed destructuring key".into(),
2227                            ));
2228                        },
2229                    };
2230                    let var_name = match kv.value.as_ref() {
2231                        Pat::Ident(ident) => ident.id.sym.to_string(),
2232                        _ => {
2233                            return Err(CompileError::UnsupportedExpression(
2234                                "nested destructuring".into(),
2235                            ));
2236                        },
2237                    };
2238                    bindings.push((var_name, key));
2239                },
2240                ObjectPatProp::Rest(_) => {
2241                    return Err(CompileError::UnsupportedExpression(
2242                        "rest pattern in destructuring".into(),
2243                    ));
2244                },
2245            }
2246        }
2247        Ok(bindings)
2248    }
2249
2250    /// Compile object destructuring: `const { a, b } = expr`
2251    /// Generates a temp var assignment + property access assignments.
2252    fn compile_object_destructuring(
2253        &mut self,
2254        obj_pat: &ObjectPat,
2255        init: &Expr,
2256        steps: &mut Vec<PlanStep>,
2257    ) -> Result<(), CompileError> {
2258        let bindings = Self::extract_object_bindings(obj_pat)?;
2259        let temp_var = self.next_temp_var();
2260
2261        // Compile the RHS into the temp var
2262        self.compile_var_init(&temp_var, init, steps)?;
2263
2264        // Generate property access assignments for each binding
2265        for (var_name, property) in bindings {
2266            steps.push(PlanStep::Assign {
2267                var: var_name,
2268                expr: ValueExpr::PropertyAccess {
2269                    object: Box::new(ValueExpr::Variable(temp_var.clone())),
2270                    property,
2271                },
2272            });
2273        }
2274        Ok(())
2275    }
2276
2277    /// Compile array destructuring: `const [a, b] = expr`
2278    /// Generates a temp var assignment + index access assignments.
2279    fn compile_array_destructuring(
2280        &mut self,
2281        arr_pat: &ArrayPat,
2282        init: &Expr,
2283        steps: &mut Vec<PlanStep>,
2284    ) -> Result<(), CompileError> {
2285        let temp_var = self.next_temp_var();
2286
2287        // Compile the RHS into the temp var
2288        self.compile_var_init(&temp_var, init, steps)?;
2289
2290        // Generate index access assignments for each element
2291        for (i, elem) in arr_pat.elems.iter().enumerate() {
2292            if let Some(pat) = elem {
2293                let var_name = self.get_var_name(pat)?;
2294                steps.push(PlanStep::Assign {
2295                    var: var_name,
2296                    expr: ValueExpr::ArrayIndex {
2297                        array: Box::new(ValueExpr::Variable(temp_var.clone())),
2298                        index: Box::new(ValueExpr::Literal(JsonValue::Number((i as i64).into()))),
2299                    },
2300                });
2301            }
2302            // None elements (holes) are skipped: `const [, b] = arr`
2303        }
2304        Ok(())
2305    }
2306
2307    fn lit_to_json(&self, lit: &Lit) -> JsonValue {
2308        match lit {
2309            Lit::Str(s) => JsonValue::String(s.value.to_string_lossy().into_owned()),
2310            Lit::Num(n) => {
2311                if n.value.fract() == 0.0 {
2312                    JsonValue::Number((n.value as i64).into())
2313                } else {
2314                    serde_json::Number::from_f64(n.value)
2315                        .map(JsonValue::Number)
2316                        .unwrap_or(JsonValue::Null)
2317                }
2318            },
2319            Lit::Bool(b) => JsonValue::Bool(b.value),
2320            Lit::Null(_) => JsonValue::Null,
2321            _ => JsonValue::Null,
2322        }
2323    }
2324
2325    fn prop_name_to_string(&self, prop: &PropName) -> Result<String, CompileError> {
2326        match prop {
2327            PropName::Ident(ident) => Ok(ident.sym.to_string()),
2328            PropName::Str(s) => Ok(s.value.to_string_lossy().into_owned()),
2329            PropName::Num(n) => Ok(n.value.to_string()),
2330            _ => Err(CompileError::UnsupportedExpression(
2331                "computed property".into(),
2332            )),
2333        }
2334    }
2335
2336    fn compile_bin_op(&self, op: BinaryOp) -> Result<BinaryOperator, CompileError> {
2337        match op {
2338            BinaryOp::Add => Ok(BinaryOperator::Add),
2339            BinaryOp::Sub => Ok(BinaryOperator::Sub),
2340            BinaryOp::Mul => Ok(BinaryOperator::Mul),
2341            BinaryOp::Div => Ok(BinaryOperator::Div),
2342            BinaryOp::Mod => Ok(BinaryOperator::Mod),
2343            BinaryOp::BitOr => Ok(BinaryOperator::BitwiseOr),
2344            BinaryOp::EqEq => Ok(BinaryOperator::Eq),
2345            BinaryOp::NotEq => Ok(BinaryOperator::NotEq),
2346            BinaryOp::EqEqEq => Ok(BinaryOperator::StrictEq),
2347            BinaryOp::NotEqEq => Ok(BinaryOperator::StrictNotEq),
2348            BinaryOp::Lt => Ok(BinaryOperator::Lt),
2349            BinaryOp::LtEq => Ok(BinaryOperator::Lte),
2350            BinaryOp::Gt => Ok(BinaryOperator::Gt),
2351            BinaryOp::GtEq => Ok(BinaryOperator::Gte),
2352            BinaryOp::LogicalAnd => Ok(BinaryOperator::And),
2353            BinaryOp::LogicalOr => Ok(BinaryOperator::Or),
2354            BinaryOp::NullishCoalescing => {
2355                // Handled separately as NullishCoalesce expr
2356                Err(CompileError::UnsupportedExpression(
2357                    "nullish coalescing".into(),
2358                ))
2359            },
2360            _ => Err(CompileError::UnsupportedExpression(format!(
2361                "binary operator {:?}",
2362                op
2363            ))),
2364        }
2365    }
2366
2367    fn record_api_call(&mut self, method: &str, path: &PathTemplate) {
2368        self.api_call_count += 1;
2369
2370        // Track methods used
2371        if !self.methods_used.contains(&method.to_string()) {
2372            self.methods_used.push(method.to_string());
2373        }
2374
2375        // Track if mutations
2376        if method != "GET" && method != "HEAD" && method != "OPTIONS" {
2377            self.has_mutations = true;
2378        }
2379
2380        // Track endpoints (simplified path for static paths)
2381        let endpoint = if !path.is_dynamic() {
2382            path.parts
2383                .iter()
2384                .filter_map(|p| match p {
2385                    PathPart::Literal(s) => Some(s.clone()),
2386                    _ => None,
2387                })
2388                .collect::<String>()
2389        } else {
2390            "{dynamic}".to_string()
2391        };
2392        if !self.endpoints.contains(&endpoint) {
2393            self.endpoints.push(endpoint);
2394        }
2395    }
2396}
2397
2398impl Default for PlanCompiler {
2399    fn default() -> Self {
2400        Self::new()
2401    }
2402}
2403
2404// ============================================================================
2405// PLAN EXECUTOR - Executes the compiled execution plan
2406// ============================================================================
2407
2408/// Trait for making HTTP requests during execution.
2409///
2410/// This abstraction allows the executor to be used with different HTTP clients
2411/// and enables easy testing with mock implementations.
2412#[async_trait::async_trait]
2413pub trait HttpExecutor: Send + Sync {
2414    /// Execute an HTTP request.
2415    async fn execute_request(
2416        &self,
2417        method: &str,
2418        path: &str,
2419        body: Option<JsonValue>,
2420    ) -> Result<JsonValue, ExecutionError>;
2421}
2422
2423/// Executor for MCP foundation server calls.
2424///
2425/// Analogous to `HttpExecutor` for API calls, this trait abstracts MCP tool
2426/// invocation for use in the AST-based executor. Implementations delegate to
2427/// actual foundation clients (e.g., `CompositionClient`).
2428#[cfg(feature = "mcp-code-mode")]
2429#[async_trait::async_trait]
2430pub trait McpExecutor: Send + Sync {
2431    /// Call a tool on a foundation server.
2432    async fn call_tool(
2433        &self,
2434        server_id: &str,
2435        tool_name: &str,
2436        args: JsonValue,
2437    ) -> Result<JsonValue, ExecutionError>;
2438}
2439
2440/// Executor for SDK-backed API calls.
2441///
2442/// Analogous to `HttpExecutor` for HTTP calls, this trait abstracts named SDK
2443/// operation invocation. Implementations route `api.<operation>(args)` to actual
2444/// SDK calls (e.g., AWS Cost Explorer).
2445#[async_trait::async_trait]
2446pub trait SdkExecutor: Send + Sync {
2447    /// Execute a named SDK operation.
2448    ///
2449    /// `operation` is the camelCase method name (e.g., "getCostAndUsage").
2450    /// `args` is the optional JSON object argument from the script.
2451    async fn execute_operation(
2452        &self,
2453        operation: &str,
2454        args: Option<JsonValue>,
2455    ) -> Result<JsonValue, ExecutionError>;
2456}
2457
2458// ============================================================================
2459// MOCK HTTP EXECUTOR - For dry-run, testing, and development
2460// ============================================================================
2461
2462/// Execution mode for the mock executor.
2463#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2464pub enum MockExecutionMode {
2465    /// Dry-run: Returns mock responses, records calls for preview.
2466    #[default]
2467    DryRun,
2468    /// Testing: Returns configured mock responses for assertions.
2469    Testing,
2470    /// Record: Passes through to a real executor and records responses.
2471    Record,
2472}
2473
2474/// Mock HTTP executor for dry-run validation and testing.
2475///
2476/// This executor doesn't make real HTTP calls. Instead, it:
2477/// - Records all API calls that would be made
2478/// - Returns configurable mock responses
2479/// - Enables dry-run validation showing what a script would do
2480///
2481/// # Example
2482///
2483/// ```ignore
2484/// use mcp_server_common::code_mode::executor::{MockHttpExecutor, PlanExecutor};
2485///
2486/// // Create a mock executor for dry-run
2487/// let mock = MockHttpExecutor::new_dry_run();
2488///
2489/// // Or with custom responses for testing
2490/// let mock = MockHttpExecutor::new_testing()
2491///     .with_response("/users", json!({"users": [{"id": 1, "name": "Test"}]}))
2492///     .with_response("/orders/*", json!({"orders": []}));
2493///
2494/// // Execute the plan
2495/// let executor = PlanExecutor::new(mock, config);
2496/// let result = executor.execute(plan).await?;
2497///
2498/// // Check what calls would be made
2499/// for call in mock.recorded_calls() {
2500///     println!("Would call: {} {}", call.method, call.path);
2501/// }
2502/// ```
2503pub struct MockHttpExecutor {
2504    /// Execution mode
2505    mode: MockExecutionMode,
2506    /// Mock responses by path pattern (exact match or glob pattern with *)
2507    responses: std::sync::RwLock<HashMap<String, JsonValue>>,
2508    /// Default response for unmatched paths
2509    default_response: JsonValue,
2510    /// Record of all calls made (method, path, body, response)
2511    recorded_calls: std::sync::RwLock<Vec<MockedCall>>,
2512}
2513
2514/// A recorded mock call with request and response.
2515#[derive(Debug, Clone, Serialize)]
2516pub struct MockedCall {
2517    /// HTTP method (GET, POST, etc.)
2518    pub method: String,
2519    /// Request path
2520    pub path: String,
2521    /// Request body if any
2522    pub body: Option<JsonValue>,
2523    /// Response returned
2524    pub response: JsonValue,
2525}
2526
2527impl MockHttpExecutor {
2528    /// Create a new mock executor for dry-run mode.
2529    /// Returns empty objects `{}` for all calls.
2530    pub fn new_dry_run() -> Self {
2531        Self {
2532            mode: MockExecutionMode::DryRun,
2533            responses: std::sync::RwLock::new(HashMap::new()),
2534            default_response: JsonValue::Object(serde_json::Map::new()),
2535            recorded_calls: std::sync::RwLock::new(Vec::new()),
2536        }
2537    }
2538
2539    /// Create a new mock executor for testing mode.
2540    /// Configure responses with `with_response()`.
2541    pub fn new_testing() -> Self {
2542        Self {
2543            mode: MockExecutionMode::Testing,
2544            responses: std::sync::RwLock::new(HashMap::new()),
2545            default_response: JsonValue::Object(serde_json::Map::new()),
2546            recorded_calls: std::sync::RwLock::new(Vec::new()),
2547        }
2548    }
2549
2550    /// Set the default response for unmatched paths.
2551    pub fn with_default_response(mut self, response: JsonValue) -> Self {
2552        self.default_response = response;
2553        self
2554    }
2555
2556    /// Add a mock response for a specific path pattern.
2557    /// Supports exact matches and simple glob patterns with `*`.
2558    ///
2559    /// # Examples
2560    ///
2561    /// ```ignore
2562    /// mock.with_response("/users", json!({"users": []}))
2563    ///     .with_response("/users/*", json!({"id": 1, "name": "Test"}))
2564    ///     .with_response("/orders/*/items", json!({"items": []}));
2565    /// ```
2566    pub fn with_response(self, path_pattern: &str, response: JsonValue) -> Self {
2567        self.responses
2568            .write()
2569            .unwrap()
2570            .insert(path_pattern.to_string(), response);
2571        self
2572    }
2573
2574    /// Add a mock response (non-builder version).
2575    pub fn add_response(&self, path_pattern: &str, response: JsonValue) {
2576        self.responses
2577            .write()
2578            .unwrap()
2579            .insert(path_pattern.to_string(), response);
2580    }
2581
2582    /// Get all recorded calls.
2583    pub fn recorded_calls(&self) -> Vec<MockedCall> {
2584        self.recorded_calls.read().unwrap().clone()
2585    }
2586
2587    /// Clear all recorded calls.
2588    pub fn clear_calls(&self) {
2589        self.recorded_calls.write().unwrap().clear();
2590    }
2591
2592    /// Get the number of calls made.
2593    pub fn call_count(&self) -> usize {
2594        self.recorded_calls.read().unwrap().len()
2595    }
2596
2597    /// Check if a specific path was called.
2598    pub fn was_called(&self, path: &str) -> bool {
2599        self.recorded_calls
2600            .read()
2601            .unwrap()
2602            .iter()
2603            .any(|c| c.path == path)
2604    }
2605
2606    /// Check if a path was called with a specific method.
2607    pub fn was_called_with_method(&self, method: &str, path: &str) -> bool {
2608        self.recorded_calls
2609            .read()
2610            .unwrap()
2611            .iter()
2612            .any(|c| c.method == method && c.path == path)
2613    }
2614
2615    /// Find the response for a path, checking patterns.
2616    fn find_response(&self, path: &str) -> JsonValue {
2617        let responses = self.responses.read().unwrap();
2618
2619        // First try exact match
2620        if let Some(response) = responses.get(path) {
2621            return response.clone();
2622        }
2623
2624        // Then try pattern matching
2625        for (pattern, response) in responses.iter() {
2626            if Self::matches_pattern(pattern, path) {
2627                return response.clone();
2628            }
2629        }
2630
2631        // Return default
2632        self.default_response.clone()
2633    }
2634
2635    /// Simple glob pattern matching (supports * as wildcard for path segments).
2636    fn matches_pattern(pattern: &str, path: &str) -> bool {
2637        if !pattern.contains('*') {
2638            return pattern == path;
2639        }
2640
2641        let pattern_parts: Vec<&str> = pattern.split('/').collect();
2642        let path_parts: Vec<&str> = path.split('/').collect();
2643
2644        if pattern_parts.len() != path_parts.len() {
2645            // Check for trailing * that matches multiple segments
2646            if pattern.ends_with("*") && path_parts.len() >= pattern_parts.len() - 1 {
2647                // Allow trailing wildcard to match remaining segments
2648            } else {
2649                return false;
2650            }
2651        }
2652
2653        for (p, s) in pattern_parts.iter().zip(path_parts.iter()) {
2654            if *p != "*" && *p != *s {
2655                return false;
2656            }
2657        }
2658
2659        true
2660    }
2661}
2662
2663#[async_trait::async_trait]
2664impl HttpExecutor for MockHttpExecutor {
2665    async fn execute_request(
2666        &self,
2667        method: &str,
2668        path: &str,
2669        body: Option<JsonValue>,
2670    ) -> Result<JsonValue, ExecutionError> {
2671        let response = self.find_response(path);
2672
2673        // Record the call
2674        let call = MockedCall {
2675            method: method.to_string(),
2676            path: path.to_string(),
2677            body,
2678            response: response.clone(),
2679        };
2680        self.recorded_calls.write().unwrap().push(call);
2681
2682        Ok(response)
2683    }
2684}
2685
2686// Implement Send + Sync (safe because we use RwLock)
2687unsafe impl Send for MockHttpExecutor {}
2688unsafe impl Sync for MockHttpExecutor {}
2689
2690/// Result of executing a plan.
2691#[derive(Debug, Clone, Serialize)]
2692pub struct ExecutionResult {
2693    /// The final return value
2694    pub value: JsonValue,
2695    /// Log of all API calls made
2696    pub api_calls: Vec<ApiCallLog>,
2697    /// Total execution time in milliseconds
2698    pub execution_time_ms: u64,
2699}
2700
2701/// Log entry for an API call.
2702#[derive(Debug, Clone, Serialize)]
2703pub struct ApiCallLog {
2704    /// HTTP method
2705    pub method: String,
2706    /// Resolved path
2707    pub path: String,
2708    /// Request body (if any)
2709    pub body: Option<JsonValue>,
2710    /// Response value
2711    pub response: JsonValue,
2712    /// Time taken in milliseconds
2713    pub duration_ms: u64,
2714}
2715
2716/// Executes a compiled execution plan.
2717pub struct PlanExecutor<H: HttpExecutor> {
2718    http: H,
2719    config: ExecutionConfig,
2720    variables: HashMap<String, JsonValue>,
2721    api_calls: Vec<ApiCallLog>,
2722    api_call_count: usize,
2723    #[cfg(feature = "mcp-code-mode")]
2724    mcp: Option<Box<dyn McpExecutor>>,
2725    /// Optional SDK executor for SDK-backed servers (e.g., aws-billing).
2726    sdk: Option<Box<dyn SdkExecutor>>,
2727}
2728
2729impl<H: HttpExecutor> PlanExecutor<H> {
2730    /// Create a new executor with the given HTTP client.
2731    pub fn new(http: H, config: ExecutionConfig) -> Self {
2732        Self {
2733            http,
2734            config,
2735            variables: HashMap::new(),
2736            api_calls: Vec::new(),
2737            api_call_count: 0,
2738            #[cfg(feature = "mcp-code-mode")]
2739            mcp: None,
2740            sdk: None,
2741        }
2742    }
2743
2744    /// Set the MCP executor for foundation server calls.
2745    #[cfg(feature = "mcp-code-mode")]
2746    pub fn set_mcp_executor(&mut self, executor: impl McpExecutor + 'static) {
2747        self.mcp = Some(Box::new(executor));
2748    }
2749
2750    /// Set the SDK executor for SDK-backed servers.
2751    pub fn set_sdk_executor(&mut self, executor: impl SdkExecutor + 'static) {
2752        self.sdk = Some(Box::new(executor));
2753    }
2754
2755    /// Pre-bind a variable before execution (e.g., `args` for script tools).
2756    pub fn set_variable(&mut self, name: impl Into<String>, value: JsonValue) {
2757        self.variables.insert(name.into(), value);
2758    }
2759
2760    /// Execute a plan and return the result.
2761    pub async fn execute(
2762        &mut self,
2763        plan: &ExecutionPlan,
2764    ) -> Result<ExecutionResult, ExecutionError> {
2765        let start = std::time::Instant::now();
2766
2767        let mut return_value = JsonValue::Null;
2768
2769        for step in &plan.steps {
2770            match self.execute_step(step).await? {
2771                StepOutcome::Return(value) => {
2772                    return_value = value;
2773                    break; // Early return — stop executing further steps
2774                },
2775                StepOutcome::None | StepOutcome::Continue | StepOutcome::Break => {},
2776            }
2777        }
2778
2779        // Validate output against output blocklist.
2780        // These are fields that can be used internally but cannot be returned.
2781        let blocked_in_output =
2782            find_blocked_fields_in_output(&return_value, &self.config.output_blocked_fields);
2783
2784        if !blocked_in_output.is_empty() {
2785            return Err(ExecutionError::RuntimeError {
2786                message: format!(
2787                    "Script output contains blocked fields: {}",
2788                    blocked_in_output.join(", ")
2789                ),
2790            });
2791        }
2792
2793        Ok(ExecutionResult {
2794            value: return_value,
2795            api_calls: std::mem::take(&mut self.api_calls),
2796            execution_time_ms: start.elapsed().as_millis() as u64,
2797        })
2798    }
2799
2800    /// Execute a single step, returning a `StepOutcome` for control flow.
2801    /// Uses Box::pin for recursive calls to avoid infinite future size.
2802    fn execute_step<'a>(
2803        &'a mut self,
2804        step: &'a PlanStep,
2805    ) -> std::pin::Pin<
2806        Box<dyn std::future::Future<Output = Result<StepOutcome, ExecutionError>> + Send + 'a>,
2807    > {
2808        Box::pin(async move {
2809            match step {
2810                PlanStep::ApiCall {
2811                    result_var,
2812                    method,
2813                    path,
2814                    body,
2815                } => {
2816                    self.api_call_count += 1;
2817                    if self.api_call_count > self.config.max_api_calls {
2818                        return Err(ExecutionError::RuntimeError {
2819                            message: format!(
2820                                "Too many API calls: {} (max: {})",
2821                                self.api_call_count, self.config.max_api_calls
2822                            ),
2823                        });
2824                    }
2825
2826                    let resolved_path = self.resolve_path(path)?;
2827                    let resolved_body = match body {
2828                        Some(expr) => Some(self.evaluate(expr)?),
2829                        None => None,
2830                    };
2831
2832                    let call_start = std::time::Instant::now();
2833                    let raw_response = self
2834                        .http
2835                        .execute_request(method, &resolved_path, resolved_body.clone())
2836                        .await
2837                        .map_err(|e| ExecutionError::RuntimeError {
2838                            message: format!("{} {} failed: {}", method, resolved_path, e),
2839                        })?;
2840                    let duration_ms = call_start.elapsed().as_millis() as u64;
2841
2842                    // Filter blocked fields from API response before scripts can access them.
2843                    // This implements the "internal blocklist" - fields that are never accessible.
2844                    let response = filter_blocked_fields(raw_response, &self.config.blocked_fields);
2845
2846                    self.api_calls.push(ApiCallLog {
2847                        method: method.clone(),
2848                        path: resolved_path,
2849                        body: resolved_body,
2850                        response: response.clone(),
2851                        duration_ms,
2852                    });
2853
2854                    if result_var != "_" {
2855                        self.variables.insert(result_var.clone(), response);
2856                    }
2857                    Ok(StepOutcome::None)
2858                },
2859
2860                PlanStep::Assign { var, expr } => {
2861                    let value = self.evaluate(expr)?;
2862                    self.variables.insert(var.clone(), value);
2863                    Ok(StepOutcome::None)
2864                },
2865
2866                PlanStep::Conditional {
2867                    condition,
2868                    then_steps,
2869                    else_steps,
2870                } => {
2871                    let cond_value = self.evaluate(condition)?;
2872                    let steps = if shared_is_truthy(&cond_value) {
2873                        then_steps
2874                    } else {
2875                        else_steps
2876                    };
2877
2878                    for step in steps {
2879                        match self.execute_step(step).await? {
2880                            StepOutcome::None => {},
2881                            outcome => return Ok(outcome),
2882                        }
2883                    }
2884                    Ok(StepOutcome::None)
2885                },
2886
2887                PlanStep::BoundedLoop {
2888                    item_var,
2889                    collection,
2890                    max_iterations,
2891                    body,
2892                } => {
2893                    let collection_value = self.evaluate(collection)?;
2894                    let items = match collection_value {
2895                        JsonValue::Array(arr) => arr,
2896                        _ => {
2897                            return Err(ExecutionError::RuntimeError {
2898                                message: "Loop collection must be an array".into(),
2899                            })
2900                        },
2901                    };
2902
2903                    let limit = (*max_iterations).min(self.config.max_loop_iterations);
2904                    'outer: for (_i, item) in items.into_iter().take(limit).enumerate() {
2905                        self.variables.insert(item_var.clone(), item);
2906
2907                        for step in body {
2908                            match self.execute_step(step).await? {
2909                                StepOutcome::Return(value) => {
2910                                    return Ok(StepOutcome::Return(value))
2911                                },
2912                                StepOutcome::None => {},
2913                                StepOutcome::Continue => continue 'outer,
2914                                StepOutcome::Break => break 'outer,
2915                            }
2916                        }
2917                    }
2918                    Ok(StepOutcome::None)
2919                },
2920
2921                PlanStep::Return { value } => {
2922                    let result = self.evaluate(value)?;
2923                    Ok(StepOutcome::Return(result))
2924                },
2925
2926                PlanStep::TryCatch {
2927                    try_steps,
2928                    catch_var,
2929                    catch_steps,
2930                    finally_steps,
2931                } => {
2932                    // Execute try block
2933                    let try_result = async {
2934                        for step in try_steps {
2935                            match self.execute_step(step).await? {
2936                                StepOutcome::None => {},
2937                                outcome => return Ok::<StepOutcome, ExecutionError>(outcome),
2938                            }
2939                        }
2940                        Ok(StepOutcome::None)
2941                    }
2942                    .await;
2943
2944                    // If try succeeded, just run finally
2945                    let result = match try_result {
2946                        Ok(outcome) => {
2947                            // Try block succeeded
2948                            outcome
2949                        },
2950                        Err(error) => {
2951                            // Try block failed, run catch
2952                            if let Some(var) = catch_var {
2953                                // Store the error in the catch variable
2954                                let error_obj = JsonValue::Object(serde_json::Map::from_iter([(
2955                                    "message".to_string(),
2956                                    JsonValue::String(format!("{}", error)),
2957                                )]));
2958                                self.variables.insert(var.clone(), error_obj);
2959                            }
2960
2961                            // Execute catch block
2962                            let mut catch_outcome = StepOutcome::None;
2963                            for step in catch_steps {
2964                                match self.execute_step(step).await? {
2965                                    StepOutcome::None => {},
2966                                    outcome => {
2967                                        catch_outcome = outcome;
2968                                        break;
2969                                    },
2970                                }
2971                            }
2972                            catch_outcome
2973                        },
2974                    };
2975
2976                    // Execute finally block (always runs)
2977                    for step in finally_steps {
2978                        match self.execute_step(step).await? {
2979                            StepOutcome::None => {},
2980                            outcome => return Ok(outcome),
2981                        }
2982                    }
2983
2984                    Ok(result)
2985                },
2986
2987                // Parallel API calls: await Promise.all([api.get(...), ...])
2988                // Executed sequentially (true parallelism isn't needed for correctness),
2989                // results collected into an array assigned to result_var.
2990                PlanStep::ParallelApiCalls { result_var, calls } => {
2991                    let mut results = Vec::with_capacity(calls.len());
2992                    for (_temp_var, method, path, body) in calls {
2993                        self.api_call_count += 1;
2994                        if self.api_call_count > self.config.max_api_calls {
2995                            return Err(ExecutionError::RuntimeError {
2996                                message: format!(
2997                                    "Maximum API calls exceeded ({})",
2998                                    self.config.max_api_calls
2999                                ),
3000                            });
3001                        }
3002
3003                        let resolved_path = self.resolve_path(path)?;
3004                        let resolved_body = body.as_ref().map(|b| self.evaluate(b)).transpose()?;
3005                        let call_start = std::time::Instant::now();
3006                        let raw_response = self
3007                            .http
3008                            .execute_request(method, &resolved_path, resolved_body.clone())
3009                            .await
3010                            .map_err(|e| ExecutionError::RuntimeError {
3011                                message: format!("{} {} failed: {}", method, resolved_path, e),
3012                            })?;
3013                        let duration_ms = call_start.elapsed().as_millis() as u64;
3014                        let response =
3015                            filter_blocked_fields(raw_response, &self.config.blocked_fields);
3016
3017                        self.api_calls.push(ApiCallLog {
3018                            method: method.clone(),
3019                            path: resolved_path,
3020                            body: resolved_body,
3021                            response: response.clone(),
3022                            duration_ms,
3023                        });
3024
3025                        results.push(response);
3026                    }
3027                    self.variables
3028                        .insert(result_var.clone(), JsonValue::Array(results));
3029                    Ok(StepOutcome::None)
3030                },
3031
3032                // Continue: signal to skip to next loop iteration
3033                PlanStep::Continue => Ok(StepOutcome::Continue),
3034
3035                // Break: signal to exit the current loop
3036                PlanStep::Break => Ok(StepOutcome::Break),
3037
3038                // MCP tool call: await mcp.call('server', 'tool', { args })
3039                #[cfg(feature = "mcp-code-mode")]
3040                PlanStep::McpCall {
3041                    result_var,
3042                    server_id,
3043                    tool_name,
3044                    args,
3045                } => {
3046                    self.api_call_count += 1;
3047                    if self.api_call_count > self.config.max_api_calls {
3048                        return Err(ExecutionError::RuntimeError {
3049                            message: format!(
3050                                "Too many calls: {} (max: {})",
3051                                self.api_call_count, self.config.max_api_calls
3052                            ),
3053                        });
3054                    }
3055
3056                    let resolved_args = match args {
3057                        Some(expr) => self.evaluate(expr)?,
3058                        None => JsonValue::Object(Default::default()),
3059                    };
3060
3061                    let mcp_executor =
3062                        self.mcp
3063                            .as_ref()
3064                            .ok_or_else(|| ExecutionError::RuntimeError {
3065                                message: "MCP executor not configured".into(),
3066                            })?;
3067
3068                    let call_start = std::time::Instant::now();
3069                    let result = mcp_executor
3070                        .call_tool(server_id, tool_name, resolved_args.clone())
3071                        .await?;
3072                    let duration_ms = call_start.elapsed().as_millis() as u64;
3073
3074                    self.api_calls.push(ApiCallLog {
3075                        method: format!("MCP:{}.{}", server_id, tool_name),
3076                        path: format!("{}/{}", server_id, tool_name),
3077                        body: Some(resolved_args),
3078                        response: result.clone(),
3079                        duration_ms,
3080                    });
3081
3082                    if result_var != "_" {
3083                        self.variables.insert(result_var.clone(), result);
3084                    }
3085                    Ok(StepOutcome::None)
3086                },
3087
3088                // SDK call: await api.getCostAndUsage({ ... })
3089                PlanStep::SdkCall {
3090                    result_var,
3091                    operation,
3092                    args,
3093                } => {
3094                    self.api_call_count += 1;
3095                    if self.api_call_count > self.config.max_api_calls {
3096                        return Err(ExecutionError::RuntimeError {
3097                            message: format!(
3098                                "Too many calls: {} (max: {})",
3099                                self.api_call_count, self.config.max_api_calls
3100                            ),
3101                        });
3102                    }
3103
3104                    let resolved_args =
3105                        args.as_ref().map(|expr| self.evaluate(expr)).transpose()?;
3106
3107                    let sdk_executor =
3108                        self.sdk
3109                            .as_ref()
3110                            .ok_or_else(|| ExecutionError::RuntimeError {
3111                                message: "SDK executor not configured".into(),
3112                            })?;
3113
3114                    let call_start = std::time::Instant::now();
3115                    let result = sdk_executor
3116                        .execute_operation(operation, resolved_args.clone())
3117                        .await?;
3118                    let duration_ms = call_start.elapsed().as_millis() as u64;
3119
3120                    self.api_calls.push(ApiCallLog {
3121                        method: operation.clone(),
3122                        path: format!("sdk:{}", operation),
3123                        body: resolved_args,
3124                        response: result.clone(),
3125                        duration_ms,
3126                    });
3127
3128                    if result_var != "_" {
3129                        self.variables.insert(result_var.clone(), result);
3130                    }
3131                    Ok(StepOutcome::None)
3132                },
3133            }
3134        })
3135    }
3136
3137    /// Resolve a path template to a concrete path string.
3138    fn resolve_path(&self, path: &PathTemplate) -> Result<String, ExecutionError> {
3139        let mut result = String::new();
3140        for part in &path.parts {
3141            match part {
3142                PathPart::Literal(s) => result.push_str(s),
3143                PathPart::Variable(var) => {
3144                    let value =
3145                        self.variables
3146                            .get(var)
3147                            .ok_or_else(|| ExecutionError::RuntimeError {
3148                                message: format!("Undefined variable in path: {}", var),
3149                            })?;
3150                    result.push_str(&shared_json_to_string_with_mode(
3151                        value,
3152                        JsonStringMode::Json,
3153                    ));
3154                },
3155                PathPart::Expression(expr) => {
3156                    let value = self.evaluate(expr)?;
3157                    result.push_str(&shared_json_to_string_with_mode(
3158                        &value,
3159                        JsonStringMode::Json,
3160                    ));
3161                },
3162            }
3163        }
3164        Ok(result)
3165    }
3166
3167    /// Evaluate an expression to a JSON value.
3168    /// Delegates to the shared evaluation module.
3169    fn evaluate(&self, expr: &ValueExpr) -> Result<JsonValue, ExecutionError> {
3170        shared_evaluate(expr, &self.variables)
3171    }
3172
3173    /// Evaluate an expression with a temporary variable binding.
3174    /// Used for array method callbacks like .map(), .filter(), etc.
3175    fn evaluate_with_binding(
3176        &self,
3177        expr: &ValueExpr,
3178        var: &str,
3179        value: &JsonValue,
3180    ) -> Result<JsonValue, ExecutionError> {
3181        shared_evaluate_with_binding(expr, &self.variables, var, value)
3182    }
3183
3184    /// Evaluate with two bindings (for reduce).
3185    fn evaluate_with_two_bindings(
3186        &self,
3187        expr: &ValueExpr,
3188        var1: &str,
3189        value1: &JsonValue,
3190        var2: &str,
3191        value2: &JsonValue,
3192    ) -> Result<JsonValue, ExecutionError> {
3193        shared_evaluate_with_two_bindings(expr, &self.variables, var1, value1, var2, value2)
3194    }
3195}
3196
3197// ============================================================================
3198// LEGACY COMPATIBILITY - Types for backward compatibility
3199// ============================================================================
3200
3201/// Legacy JsExecutor type alias for backward compatibility.
3202pub type JsExecutor = PlanCompiler;
3203
3204#[cfg(test)]
3205mod tests {
3206    use super::*;
3207
3208    #[test]
3209    fn test_execution_config_default() {
3210        let config = ExecutionConfig::default();
3211        assert_eq!(config.max_api_calls, 50);
3212        assert_eq!(config.timeout_seconds, 30);
3213        assert_eq!(config.max_loop_iterations, 100);
3214    }
3215
3216    #[test]
3217    fn test_path_template_static() {
3218        let path = PathTemplate::static_path("/users".into());
3219        assert!(!path.is_dynamic());
3220    }
3221
3222    #[test]
3223    fn test_path_template_dynamic() {
3224        let path = PathTemplate {
3225            parts: vec![
3226                PathPart::Literal("/users/".into()),
3227                PathPart::Variable("id".into()),
3228            ],
3229        };
3230        assert!(path.is_dynamic());
3231    }
3232
3233    #[test]
3234    fn test_plan_metadata() {
3235        let metadata = PlanMetadata {
3236            api_call_count: 2,
3237            has_mutations: false,
3238            endpoints: vec!["/users".into(), "/products".into()],
3239            methods_used: vec!["GET".into()],
3240        };
3241        assert_eq!(metadata.api_call_count, 2);
3242        assert!(!metadata.has_mutations);
3243    }
3244
3245    #[test]
3246    fn test_compile_simple_api_call() {
3247        let code = r#"
3248            const user = await api.get('/users/1');
3249            return user;
3250        "#;
3251
3252        let mut compiler = PlanCompiler::new();
3253        let plan = compiler.compile_code(code).expect("Should compile");
3254
3255        assert_eq!(plan.metadata.api_call_count, 1);
3256        assert!(!plan.metadata.has_mutations);
3257        assert_eq!(plan.steps.len(), 2); // ApiCall + Return
3258    }
3259
3260    #[test]
3261    fn test_compile_multiple_api_calls() {
3262        let code = r#"
3263            const users = await api.get('/users');
3264            const products = await api.get('/products');
3265            return { users, products };
3266        "#;
3267
3268        let mut compiler = PlanCompiler::new();
3269        let plan = compiler.compile_code(code).expect("Should compile");
3270
3271        assert_eq!(plan.metadata.api_call_count, 2);
3272        assert!(!plan.metadata.has_mutations);
3273    }
3274
3275    #[test]
3276    fn test_compile_mutation() {
3277        let code = r#"
3278            const result = await api.post('/users', { name: 'Test' });
3279            return result;
3280        "#;
3281
3282        let mut compiler = PlanCompiler::new();
3283        let plan = compiler.compile_code(code).expect("Should compile");
3284
3285        assert_eq!(plan.metadata.api_call_count, 1);
3286        assert!(plan.metadata.has_mutations);
3287    }
3288
3289    #[test]
3290    fn test_compile_dynamic_path() {
3291        let code = r#"
3292            const id = 123;
3293            const user = await api.get(`/users/${id}`);
3294            return user;
3295        "#;
3296
3297        let mut compiler = PlanCompiler::new();
3298        let plan = compiler.compile_code(code).expect("Should compile");
3299
3300        assert_eq!(plan.metadata.api_call_count, 1);
3301    }
3302
3303    #[test]
3304    fn test_compile_bounded_loop() {
3305        let code = r#"
3306            const items = [];
3307            const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3308            for (const user of users.slice(0, 2)) {
3309                const detail = await api.get(`/users/${user.id}`);
3310                items.push(detail);
3311            }
3312            return items;
3313        "#;
3314
3315        let mut compiler = PlanCompiler::new();
3316        let plan = compiler.compile_code(code).expect("Should compile");
3317
3318        // The loop is bounded, so it should compile
3319        assert!(plan
3320            .steps
3321            .iter()
3322            .any(|s| matches!(s, PlanStep::BoundedLoop { .. })));
3323    }
3324
3325    #[test]
3326    fn test_compile_unbounded_loop_detection() {
3327        // Note: The current compiler allows for-of loops without explicit .slice() bounds
3328        // as long as the loop body doesn't exceed iteration limits at runtime.
3329        // This test documents the current behavior.
3330        let code = r#"
3331            const users = [{ id: 1 }, { id: 2 }, { id: 3 }];
3332            for (const user of users) {
3333                const detail = await api.get(`/users/${user.id}`);
3334            }
3335            return users;
3336        "#;
3337
3338        let mut compiler = PlanCompiler::new();
3339        let result = compiler.compile_code(code);
3340
3341        // Currently this compiles - runtime will enforce iteration limits
3342        // TODO: Consider adding compile-time bounds checking
3343        assert!(result.is_ok(), "Loop compiled: {:?}", result);
3344    }
3345
3346    #[test]
3347    fn test_compile_conditional() {
3348        let code = r#"
3349            const user = await api.get('/users/1');
3350            if (user.active) {
3351                const orders = await api.get(`/users/${user.id}/orders`);
3352                return orders;
3353            } else {
3354                return [];
3355            }
3356        "#;
3357
3358        let mut compiler = PlanCompiler::new();
3359        let plan = compiler.compile_code(code).expect("Should compile");
3360
3361        assert!(plan
3362            .steps
3363            .iter()
3364            .any(|s| matches!(s, PlanStep::Conditional { .. })));
3365    }
3366
3367    // Mock HTTP executor for testing
3368    struct MockHttpExecutor {
3369        responses: std::collections::HashMap<String, JsonValue>,
3370    }
3371
3372    impl MockHttpExecutor {
3373        fn new() -> Self {
3374            Self {
3375                responses: std::collections::HashMap::new(),
3376            }
3377        }
3378
3379        fn add_response(&mut self, path: &str, response: JsonValue) {
3380            self.responses.insert(path.to_string(), response);
3381        }
3382    }
3383
3384    #[async_trait::async_trait]
3385    impl HttpExecutor for MockHttpExecutor {
3386        async fn execute_request(
3387            &self,
3388            _method: &str,
3389            path: &str,
3390            _body: Option<JsonValue>,
3391        ) -> Result<JsonValue, ExecutionError> {
3392            self.responses
3393                .get(path)
3394                .cloned()
3395                .ok_or_else(|| ExecutionError::RuntimeError {
3396                    message: format!("No mock response for path: {}", path),
3397                })
3398        }
3399    }
3400
3401    #[tokio::test]
3402    async fn test_execute_simple_api_call() {
3403        let code = r#"
3404            const user = await api.get('/users/1');
3405            return user;
3406        "#;
3407
3408        let mut compiler = PlanCompiler::new();
3409        let plan = compiler.compile_code(code).expect("Should compile");
3410
3411        let mut mock_http = MockHttpExecutor::new();
3412        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "name": "Alice" }));
3413
3414        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3415        let result = executor.execute(&plan).await.expect("Should execute");
3416
3417        assert_eq!(result.value["id"], 1);
3418        assert_eq!(result.value["name"], "Alice");
3419        assert_eq!(result.api_calls.len(), 1);
3420    }
3421
3422    #[tokio::test]
3423    async fn test_execute_multiple_api_calls() {
3424        let code = r#"
3425            const users = await api.get('/users');
3426            const products = await api.get('/products');
3427            return { users, products };
3428        "#;
3429
3430        let mut compiler = PlanCompiler::new();
3431        let plan = compiler.compile_code(code).expect("Should compile");
3432
3433        let mut mock_http = MockHttpExecutor::new();
3434        mock_http.add_response("/users", serde_json::json!([{ "id": 1, "name": "Alice" }]));
3435        mock_http.add_response(
3436            "/products",
3437            serde_json::json!([{ "id": 100, "name": "Widget" }]),
3438        );
3439
3440        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3441        let result = executor.execute(&plan).await.expect("Should execute");
3442
3443        assert!(result.value["users"].is_array());
3444        assert!(result.value["products"].is_array());
3445        assert_eq!(result.api_calls.len(), 2);
3446    }
3447
3448    #[tokio::test]
3449    async fn test_execute_with_template_path() {
3450        let code = r#"
3451            const userId = 42;
3452            const user = await api.get(`/users/${userId}`);
3453            return user;
3454        "#;
3455
3456        let mut compiler = PlanCompiler::new();
3457        let plan = compiler.compile_code(code).expect("Should compile");
3458
3459        let mut mock_http = MockHttpExecutor::new();
3460        mock_http.add_response("/users/42", serde_json::json!({ "id": 42, "name": "Bob" }));
3461
3462        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3463        let result = executor.execute(&plan).await.expect("Should execute");
3464
3465        assert_eq!(result.value["id"], 42);
3466        assert_eq!(result.value["name"], "Bob");
3467    }
3468
3469    #[tokio::test]
3470    async fn test_execute_conditional_true_branch() {
3471        let code = r#"
3472            const user = await api.get('/users/1');
3473            if (user.active) {
3474                return { status: "active", user: user };
3475            } else {
3476                return { status: "inactive" };
3477            }
3478        "#;
3479
3480        let mut compiler = PlanCompiler::new();
3481        let plan = compiler.compile_code(code).expect("Should compile");
3482
3483        let mut mock_http = MockHttpExecutor::new();
3484        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": true }));
3485
3486        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3487        let result = executor.execute(&plan).await.expect("Should execute");
3488
3489        assert_eq!(result.value["status"], "active");
3490    }
3491
3492    #[tokio::test]
3493    async fn test_execute_conditional_false_branch() {
3494        let code = r#"
3495            const user = await api.get('/users/1');
3496            if (user.active) {
3497                return { status: "active" };
3498            } else {
3499                return { status: "inactive", user: user };
3500            }
3501        "#;
3502
3503        let mut compiler = PlanCompiler::new();
3504        let plan = compiler.compile_code(code).expect("Should compile");
3505
3506        let mut mock_http = MockHttpExecutor::new();
3507        mock_http.add_response("/users/1", serde_json::json!({ "id": 1, "active": false }));
3508
3509        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3510        let result = executor.execute(&plan).await.expect("Should execute");
3511
3512        assert_eq!(result.value["status"], "inactive");
3513    }
3514
3515    #[tokio::test]
3516    async fn test_compile_and_execute_reduce() {
3517        let code = r#"
3518            const products = await api.get('/products');
3519            const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3520            return { total: totalPrice };
3521        "#;
3522
3523        let mut compiler = PlanCompiler::new();
3524        let plan = compiler.compile_code(code).expect("Should compile reduce");
3525
3526        let mut mock_http = MockHttpExecutor::new();
3527        mock_http.add_response(
3528            "/products",
3529            serde_json::json!([
3530                { "id": 1, "name": "Widget", "price": 10 },
3531                { "id": 2, "name": "Gadget", "price": 25 },
3532                { "id": 3, "name": "Gizmo", "price": 15 }
3533            ]),
3534        );
3535
3536        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3537        let result = executor.execute(&plan).await.expect("Should execute");
3538
3539        // Result is f64, compare as number
3540        assert_eq!(result.value["total"].as_f64().unwrap(), 50.0);
3541    }
3542
3543    #[tokio::test]
3544    async fn test_compile_and_execute_to_fixed() {
3545        let code = r#"
3546            const products = await api.get('/products');
3547            const totalPrice = products.reduce((sum, p) => sum + p.price, 0);
3548            const averagePrice = products.length > 0 ? totalPrice / products.length : 0;
3549            return { averagePrice: averagePrice.toFixed(2) };
3550        "#;
3551
3552        let mut compiler = PlanCompiler::new();
3553        let plan = compiler.compile_code(code).expect("Should compile toFixed");
3554
3555        let mut mock_http = MockHttpExecutor::new();
3556        mock_http.add_response(
3557            "/products",
3558            serde_json::json!([
3559                { "id": 1, "name": "Widget", "price": 10 },
3560                { "id": 2, "name": "Gadget", "price": 25 },
3561                { "id": 3, "name": "Gizmo", "price": 15 }
3562            ]),
3563        );
3564
3565        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3566        let result = executor.execute(&plan).await.expect("Should execute");
3567
3568        // 50 / 3 = 16.666... toFixed(2) = "16.67"
3569        assert_eq!(result.value["averagePrice"], "16.67");
3570    }
3571
3572    // =========================================================================
3573    // Field Filtering Tests
3574    // =========================================================================
3575
3576    #[test]
3577    fn test_filter_blocked_fields_simple() {
3578        let value = serde_json::json!({
3579            "id": 1,
3580            "name": "Alice",
3581            "password": "secret123",
3582            "email": "alice@example.com"
3583        });
3584
3585        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3586        let filtered = filter_blocked_fields(value, &blocked);
3587
3588        assert_eq!(filtered["id"], 1);
3589        assert_eq!(filtered["name"], "Alice");
3590        assert_eq!(filtered["email"], "alice@example.com");
3591        assert!(filtered.get("password").is_none());
3592    }
3593
3594    #[test]
3595    fn test_filter_blocked_fields_multiple() {
3596        let value = serde_json::json!({
3597            "id": 1,
3598            "name": "Alice",
3599            "password": "secret123",
3600            "ssn": "123-45-6789",
3601            "apiKey": "key-abc123"
3602        });
3603
3604        let blocked: HashSet<String> = ["password", "ssn", "apiKey"]
3605            .iter()
3606            .map(|s| s.to_string())
3607            .collect();
3608        let filtered = filter_blocked_fields(value, &blocked);
3609
3610        assert_eq!(filtered["id"], 1);
3611        assert_eq!(filtered["name"], "Alice");
3612        assert!(filtered.get("password").is_none());
3613        assert!(filtered.get("ssn").is_none());
3614        assert!(filtered.get("apiKey").is_none());
3615    }
3616
3617    #[test]
3618    fn test_filter_blocked_fields_nested() {
3619        let value = serde_json::json!({
3620            "user": {
3621                "id": 1,
3622                "profile": {
3623                    "name": "Alice",
3624                    "password": "secret123"
3625                }
3626            }
3627        });
3628
3629        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3630        let filtered = filter_blocked_fields(value, &blocked);
3631
3632        assert_eq!(filtered["user"]["id"], 1);
3633        assert_eq!(filtered["user"]["profile"]["name"], "Alice");
3634        assert!(filtered["user"]["profile"].get("password").is_none());
3635    }
3636
3637    #[test]
3638    fn test_filter_blocked_fields_in_array() {
3639        let value = serde_json::json!([
3640            { "id": 1, "name": "Alice", "password": "secret1" },
3641            { "id": 2, "name": "Bob", "password": "secret2" }
3642        ]);
3643
3644        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3645        let filtered = filter_blocked_fields(value, &blocked);
3646
3647        let arr = filtered.as_array().unwrap();
3648        assert_eq!(arr.len(), 2);
3649        assert_eq!(arr[0]["id"], 1);
3650        assert_eq!(arr[0]["name"], "Alice");
3651        assert!(arr[0].get("password").is_none());
3652        assert_eq!(arr[1]["id"], 2);
3653        assert_eq!(arr[1]["name"], "Bob");
3654        assert!(arr[1].get("password").is_none());
3655    }
3656
3657    #[test]
3658    fn test_filter_blocked_fields_empty_blocklist() {
3659        let value = serde_json::json!({
3660            "id": 1,
3661            "password": "secret123"
3662        });
3663
3664        let blocked: HashSet<String> = HashSet::new();
3665        let filtered = filter_blocked_fields(value.clone(), &blocked);
3666
3667        // Should be unchanged
3668        assert_eq!(filtered, value);
3669    }
3670
3671    #[test]
3672    fn test_filter_blocked_fields_primitive_values() {
3673        // Primitives should pass through unchanged
3674        let blocked: HashSet<String> = ["password"].iter().map(|s| s.to_string()).collect();
3675
3676        assert_eq!(
3677            filter_blocked_fields(JsonValue::String("test".into()), &blocked),
3678            JsonValue::String("test".into())
3679        );
3680        assert_eq!(
3681            filter_blocked_fields(JsonValue::Number(42.into()), &blocked),
3682            JsonValue::Number(42.into())
3683        );
3684        assert_eq!(
3685            filter_blocked_fields(JsonValue::Bool(true), &blocked),
3686            JsonValue::Bool(true)
3687        );
3688        assert_eq!(
3689            filter_blocked_fields(JsonValue::Null, &blocked),
3690            JsonValue::Null
3691        );
3692    }
3693
3694    #[tokio::test]
3695    async fn test_execute_with_blocked_fields() {
3696        let code = r#"
3697            const user = await api.get('/users/1');
3698            return user;
3699        "#;
3700
3701        let mut compiler = PlanCompiler::new();
3702        let plan = compiler.compile_code(code).expect("Should compile");
3703
3704        let mut mock_http = MockHttpExecutor::new();
3705        mock_http.add_response(
3706            "/users/1",
3707            serde_json::json!({
3708                "id": 1,
3709                "name": "Alice",
3710                "password": "secret123",
3711                "apiKey": "key-abc"
3712            }),
3713        );
3714
3715        // Create config with blocked fields
3716        let config = ExecutionConfig::default().with_blocked_fields(["password", "apiKey"]);
3717
3718        let mut executor = PlanExecutor::new(mock_http, config);
3719        let result = executor.execute(&plan).await.expect("Should execute");
3720
3721        // Blocked fields should be filtered out
3722        assert_eq!(result.value["id"], 1);
3723        assert_eq!(result.value["name"], "Alice");
3724        assert!(result.value.get("password").is_none());
3725        assert!(result.value.get("apiKey").is_none());
3726    }
3727
3728    #[tokio::test]
3729    async fn test_execute_nested_blocked_fields() {
3730        let code = r#"
3731            const data = await api.get('/data');
3732            return data;
3733        "#;
3734
3735        let mut compiler = PlanCompiler::new();
3736        let plan = compiler.compile_code(code).expect("Should compile");
3737
3738        let mut mock_http = MockHttpExecutor::new();
3739        mock_http.add_response(
3740            "/data",
3741            serde_json::json!({
3742                "users": [
3743                    { "id": 1, "name": "Alice", "secret": "hidden1" },
3744                    { "id": 2, "name": "Bob", "secret": "hidden2" }
3745                ],
3746                "config": {
3747                    "setting": "value",
3748                    "secret": "also-hidden"
3749                }
3750            }),
3751        );
3752
3753        // Create config with blocked fields
3754        let config = ExecutionConfig::default().with_blocked_fields(["secret"]);
3755
3756        let mut executor = PlanExecutor::new(mock_http, config);
3757        let result = executor.execute(&plan).await.expect("Should execute");
3758
3759        // Secret should be filtered from all nested locations
3760        let users = result.value["users"].as_array().unwrap();
3761        assert_eq!(users[0]["name"], "Alice");
3762        assert!(users[0].get("secret").is_none());
3763        assert_eq!(users[1]["name"], "Bob");
3764        assert!(users[1].get("secret").is_none());
3765
3766        assert_eq!(result.value["config"]["setting"], "value");
3767        assert!(result.value["config"].get("secret").is_none());
3768    }
3769
3770    // =========================================================================
3771    // Output Validation Tests
3772    // =========================================================================
3773
3774    #[test]
3775    fn test_find_blocked_fields_in_output_simple() {
3776        let value = serde_json::json!({
3777            "id": 1,
3778            "name": "Alice",
3779            "ssn": "123-45-6789"
3780        });
3781
3782        let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3783        let violations = find_blocked_fields_in_output(&value, &blocked);
3784
3785        assert_eq!(violations.len(), 1);
3786        assert_eq!(violations[0], "ssn");
3787    }
3788
3789    #[test]
3790    fn test_find_blocked_fields_in_output_nested() {
3791        let value = serde_json::json!({
3792            "user": {
3793                "profile": {
3794                    "name": "Alice",
3795                    "salary": 100000
3796                }
3797            }
3798        });
3799
3800        let blocked: HashSet<String> = ["salary"].iter().map(|s| s.to_string()).collect();
3801        let violations = find_blocked_fields_in_output(&value, &blocked);
3802
3803        assert_eq!(violations.len(), 1);
3804        assert!(violations[0].contains("salary"));
3805    }
3806
3807    #[test]
3808    fn test_find_blocked_fields_in_output_array() {
3809        let value = serde_json::json!([
3810            { "id": 1, "ssn": "111" },
3811            { "id": 2, "ssn": "222" }
3812        ]);
3813
3814        let blocked: HashSet<String> = ["ssn"].iter().map(|s| s.to_string()).collect();
3815        let violations = find_blocked_fields_in_output(&value, &blocked);
3816
3817        // Should find ssn in both array elements
3818        assert_eq!(violations.len(), 2);
3819    }
3820
3821    #[test]
3822    fn test_find_blocked_fields_in_output_empty_blocklist() {
3823        let value = serde_json::json!({
3824            "id": 1,
3825            "ssn": "123-45-6789"
3826        });
3827
3828        let blocked: HashSet<String> = HashSet::new();
3829        let violations = find_blocked_fields_in_output(&value, &blocked);
3830
3831        assert!(violations.is_empty());
3832    }
3833
3834    #[test]
3835    fn test_find_blocked_fields_in_output_no_violations() {
3836        let value = serde_json::json!({
3837            "id": 1,
3838            "name": "Alice"
3839        });
3840
3841        let blocked: HashSet<String> = ["ssn", "salary"].iter().map(|s| s.to_string()).collect();
3842        let violations = find_blocked_fields_in_output(&value, &blocked);
3843
3844        assert!(violations.is_empty());
3845    }
3846
3847    #[tokio::test]
3848    async fn test_execute_output_blocked_fields_rejected() {
3849        let code = r#"
3850            const user = await api.get('/users/1');
3851            return { name: user.name, ssn: user.ssn };
3852        "#;
3853
3854        let mut compiler = PlanCompiler::new();
3855        let plan = compiler.compile_code(code).expect("Should compile");
3856
3857        let mut mock_http = MockHttpExecutor::new();
3858        mock_http.add_response(
3859            "/users/1",
3860            serde_json::json!({
3861                "id": 1,
3862                "name": "Alice",
3863                "ssn": "123-45-6789"
3864            }),
3865        );
3866
3867        // Note: internal blocklist is empty, so ssn gets through to the script
3868        // But output blocklist should catch it in the return value
3869        let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3870
3871        let mut executor = PlanExecutor::new(mock_http, config);
3872        let result = executor.execute(&plan).await;
3873
3874        // Should fail because output contains blocked field
3875        assert!(result.is_err());
3876        let err = result.unwrap_err();
3877        assert!(format!("{:?}", err).contains("ssn"));
3878    }
3879
3880    #[tokio::test]
3881    async fn test_execute_output_blocked_fields_internal_use_allowed() {
3882        // Script reads user data but only returns safe fields - should succeed
3883        let code = r#"
3884            const user = await api.get('/users/1');
3885            return { id: user.id, name: user.name };
3886        "#;
3887
3888        let mut compiler = PlanCompiler::new();
3889        let plan = compiler.compile_code(code).expect("Should compile");
3890
3891        let mut mock_http = MockHttpExecutor::new();
3892        mock_http.add_response(
3893            "/users/1",
3894            serde_json::json!({
3895                "id": 1,
3896                "name": "Alice",
3897                "ssn": "123-45-6789"
3898            }),
3899        );
3900
3901        // Output blocklist - ssn can be read but not returned
3902        // Note: This doesn't prevent script from accessing ssn, just returning it
3903        let config = ExecutionConfig::default().with_output_blocked_fields(["ssn"]);
3904
3905        let mut executor = PlanExecutor::new(mock_http, config);
3906        let result = executor.execute(&plan).await.expect("Should succeed");
3907
3908        // Script read user data but only returned safe fields
3909        assert_eq!(result.value["id"], 1);
3910        assert_eq!(result.value["name"], "Alice");
3911        assert!(result.value.get("ssn").is_none());
3912    }
3913
3914    #[tokio::test]
3915    async fn test_execute_both_blocklists() {
3916        // Test that internal blocklist AND output blocklist work together
3917        let code = r#"
3918            const user = await api.get('/users/1');
3919            return { name: user.name, dateOfBirth: user.dateOfBirth };
3920        "#;
3921
3922        let mut compiler = PlanCompiler::new();
3923        let plan = compiler.compile_code(code).expect("Should compile");
3924
3925        let mut mock_http = MockHttpExecutor::new();
3926        mock_http.add_response(
3927            "/users/1",
3928            serde_json::json!({
3929                "id": 1,
3930                "name": "Alice",
3931                "password": "secret123",
3932                "dateOfBirth": "1990-01-01"
3933            }),
3934        );
3935
3936        // Internal blocklist: password is stripped from API response
3937        // Output blocklist: dateOfBirth can be used but not returned
3938        let config = ExecutionConfig::default()
3939            .with_blocked_fields(["password"])
3940            .with_output_blocked_fields(["dateOfBirth"]);
3941
3942        let mut executor = PlanExecutor::new(mock_http, config);
3943        let result = executor.execute(&plan).await;
3944
3945        // Should fail because output contains dateOfBirth
3946        assert!(result.is_err());
3947        let err = result.unwrap_err();
3948        assert!(format!("{:?}", err).contains("dateOfBirth"));
3949    }
3950
3951    // ========================================================================
3952    // Tests for pre-bound variables (args) and conditionals
3953    // ========================================================================
3954
3955    #[tokio::test]
3956    async fn test_prebound_args_comparison() {
3957        // Test that `if (args.k > args.n)` works with pre-bound args
3958        let code = r#"
3959            if (args.k > args.n) {
3960                return { error: 'k must be <= n' };
3961            }
3962            return { ok: true };
3963        "#;
3964
3965        let mut compiler = PlanCompiler::new();
3966        let plan = compiler.compile_code(code).expect("Should compile");
3967
3968        let mock_http = MockHttpExecutor::new();
3969        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3970        executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
3971
3972        let result = executor.execute(&plan).await.expect("Should execute");
3973        assert_eq!(
3974            result.value["error"], "k must be <= n",
3975            "Expected error for k > n, got: {:?}",
3976            result.value
3977        );
3978    }
3979
3980    #[tokio::test]
3981    async fn test_prebound_args_strict_equality() {
3982        // Test that `args.k === 0` works
3983        let code = r#"
3984            if (args.k === 0) {
3985                return { result: 1 };
3986            }
3987            return { result: 'not zero' };
3988        "#;
3989
3990        let mut compiler = PlanCompiler::new();
3991        let plan = compiler.compile_code(code).expect("Should compile");
3992
3993        let mock_http = MockHttpExecutor::new();
3994        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
3995        executor.set_variable("args", serde_json::json!({"k": 0}));
3996
3997        let result = executor.execute(&plan).await.expect("Should execute");
3998        assert_eq!(result.value["result"], 1);
3999    }
4000
4001    #[tokio::test]
4002    async fn test_assignment_expression_in_statement() {
4003        // Test that `k = newValue` works as a statement (Expr::Assign)
4004        let code = r#"
4005            let k = 5;
4006            k = 2;
4007            return { k: k };
4008        "#;
4009
4010        let mut compiler = PlanCompiler::new();
4011        let plan = compiler.compile_code(code).expect("Should compile");
4012
4013        let mock_http = MockHttpExecutor::new();
4014        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4015
4016        let result = executor.execute(&plan).await.expect("Should execute");
4017        assert_eq!(result.value["k"], 2);
4018    }
4019
4020    #[tokio::test]
4021    async fn test_assignment_swap_variables() {
4022        // Test swapping two let-bound variables
4023        let code = r#"
4024            let a = 3;
4025            let b = 7;
4026            if (a < b) {
4027                const old_a = a;
4028                a = b;
4029                b = old_a;
4030            }
4031            return { a: a, b: b };
4032        "#;
4033
4034        let mut compiler = PlanCompiler::new();
4035        let plan = compiler.compile_code(code).expect("Should compile");
4036
4037        let mock_http = MockHttpExecutor::new();
4038        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4039
4040        let result = executor.execute(&plan).await.expect("Should execute");
4041        assert_eq!(result.value["a"], 7);
4042        assert_eq!(result.value["b"], 3);
4043    }
4044
4045    // ========================================================================
4046    // MCP call tests (require mcp-code-mode feature)
4047    // ========================================================================
4048
4049    #[cfg(feature = "mcp-code-mode")]
4050    mod mcp_tests {
4051        use super::*;
4052
4053        /// Mock MCP executor that simulates a calculator server.
4054        struct MockCalculatorExecutor;
4055
4056        #[async_trait::async_trait]
4057        impl McpExecutor for MockCalculatorExecutor {
4058            async fn call_tool(
4059                &self,
4060                _server_id: &str,
4061                tool_name: &str,
4062                args: JsonValue,
4063            ) -> Result<JsonValue, ExecutionError> {
4064                match tool_name {
4065                    "add" => {
4066                        let a = args["a"].as_f64().unwrap_or(0.0);
4067                        let b = args["b"].as_f64().unwrap_or(0.0);
4068                        Ok(serde_json::json!({"result": a + b}))
4069                    },
4070                    "subtract" => {
4071                        let a = args["a"].as_f64().unwrap_or(0.0);
4072                        let b = args["b"].as_f64().unwrap_or(0.0);
4073                        Ok(serde_json::json!({"result": a - b}))
4074                    },
4075                    "multiply" => {
4076                        let a = args["a"].as_f64().unwrap_or(0.0);
4077                        let b = args["b"].as_f64().unwrap_or(0.0);
4078                        Ok(serde_json::json!({"result": a * b}))
4079                    },
4080                    "divide" => {
4081                        let a = args["a"].as_f64().unwrap_or(0.0);
4082                        let b = args["b"].as_f64().unwrap_or(1.0);
4083                        Ok(serde_json::json!({"result": a / b}))
4084                    },
4085                    "power" => {
4086                        let base = args["base"].as_f64().unwrap_or(0.0);
4087                        let exponent = args["exponent"].as_f64().unwrap_or(1.0);
4088                        Ok(serde_json::json!({"result": base.powf(exponent)}))
4089                    },
4090                    "sqrt" => {
4091                        let n = args["n"].as_f64().unwrap_or(0.0);
4092                        Ok(serde_json::json!({"result": n.sqrt()}))
4093                    },
4094                    _ => Err(ExecutionError::RuntimeError {
4095                        message: format!("Unknown tool: {}", tool_name),
4096                    }),
4097                }
4098            }
4099        }
4100
4101        #[tokio::test]
4102        async fn test_mcp_call_simple() {
4103            let code = r#"
4104                const result = await mcp.call('calculator', 'add', { a: 5, b: 3 });
4105                return result;
4106            "#;
4107
4108            let mut compiler = PlanCompiler::new();
4109            let plan = compiler.compile_code(code).expect("Should compile");
4110
4111            let mock_http = MockHttpExecutor::new();
4112            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4113            executor.set_mcp_executor(MockCalculatorExecutor);
4114
4115            let result = executor.execute(&plan).await.expect("Should execute");
4116            assert_eq!(result.value["result"], 8.0);
4117        }
4118
4119        #[tokio::test]
4120        async fn test_mcp_call_with_args() {
4121            // Test mcp.call using pre-bound args variable
4122            let code = r#"
4123                const result = await mcp.call('calculator', 'add', { a: args.x, b: args.y });
4124                return { sum: result.result };
4125            "#;
4126
4127            let mut compiler = PlanCompiler::new();
4128            let plan = compiler.compile_code(code).expect("Should compile");
4129
4130            let mock_http = MockHttpExecutor::new();
4131            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4132            executor.set_mcp_executor(MockCalculatorExecutor);
4133            executor.set_variable("args", serde_json::json!({"x": 10, "y": 20}));
4134
4135            let result = executor.execute(&plan).await.expect("Should execute");
4136            assert_eq!(result.value["sum"], 30.0);
4137        }
4138
4139        #[tokio::test]
4140        async fn test_mcp_assignment_in_loop() {
4141            // Test `result = await mcp.call(...)` assignment inside a loop
4142            let code = r#"
4143                let result = { result: 1 };
4144                for (const i of [2, 3, 4, 5]) {
4145                    const mul = await mcp.call('calculator', 'multiply', { a: result.result, b: i });
4146                    result = mul;
4147                }
4148                return { factorial: result.result };
4149            "#;
4150
4151            let mut compiler = PlanCompiler::new();
4152            let plan = compiler.compile_code(code).expect("Should compile");
4153
4154            let mock_http = MockHttpExecutor::new();
4155            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4156            executor.set_mcp_executor(MockCalculatorExecutor);
4157
4158            let result = executor.execute(&plan).await.expect("Should execute");
4159            // 1 * 2 * 3 * 4 * 5 = 120
4160            assert_eq!(result.value["factorial"], 120.0);
4161        }
4162
4163        #[tokio::test]
4164        async fn test_combinations_c_5_3() {
4165            // Full combinations script: C(5,3) = 10
4166            let code = r#"
4167if (args.k > args.n) {
4168  return { error: 'k must be <= n', n: args.n, k: args.k };
4169}
4170if (args.k === 0 || args.k === args.n) {
4171  return { n: args.n, k: args.k, result: 1 };
4172}
4173let k = args.k;
4174const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4175let nmk = complement.result;
4176if (nmk < k) {
4177  const old_k = k;
4178  k = nmk;
4179  nmk = old_k;
4180}
4181let result = { result: 1 };
4182for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4183  if (i > k) { break; }
4184  const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4185  const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4186  result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4187}
4188return { n: args.n, k: args.k, result: result.result };
4189            "#;
4190
4191            let mut compiler = PlanCompiler::new();
4192            let plan = compiler.compile_code(code).expect("Should compile");
4193
4194            let mock_http = MockHttpExecutor::new();
4195            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4196            executor.set_mcp_executor(MockCalculatorExecutor);
4197            executor.set_variable("args", serde_json::json!({"n": 5, "k": 3}));
4198
4199            let result = executor.execute(&plan).await.expect("Should execute");
4200            assert_eq!(
4201                result.value["result"], 10.0,
4202                "C(5,3) should be 10, got: {:?}",
4203                result.value
4204            );
4205        }
4206
4207        #[tokio::test]
4208        async fn test_combinations_k_greater_than_n() {
4209            // C(3,5) should return error
4210            let code = r#"
4211if (args.k > args.n) {
4212  return { error: 'k must be <= n', n: args.n, k: args.k };
4213}
4214return { result: 'should not reach here' };
4215            "#;
4216
4217            let mut compiler = PlanCompiler::new();
4218            let plan = compiler.compile_code(code).expect("Should compile");
4219
4220            let mock_http = MockHttpExecutor::new();
4221            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4222            executor.set_mcp_executor(MockCalculatorExecutor);
4223            executor.set_variable("args", serde_json::json!({"n": 3, "k": 5}));
4224
4225            let result = executor.execute(&plan).await.expect("Should execute");
4226            assert_eq!(
4227                result.value["error"], "k must be <= n",
4228                "C(3,5) should return error, got: {:?}",
4229                result.value
4230            );
4231        }
4232
4233        #[tokio::test]
4234        async fn test_combinations_c_5_2() {
4235            // C(5,2) = 10 — no swap needed
4236            let code = r#"
4237if (args.k > args.n) {
4238  return { error: 'k must be <= n', n: args.n, k: args.k };
4239}
4240if (args.k === 0 || args.k === args.n) {
4241  return { n: args.n, k: args.k, result: 1 };
4242}
4243let k = args.k;
4244const complement = await mcp.call('calculator', 'subtract', { a: args.n, b: args.k });
4245let nmk = complement.result;
4246if (nmk < k) {
4247  const old_k = k;
4248  k = nmk;
4249  nmk = old_k;
4250}
4251let result = { result: 1 };
4252for (const i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]) {
4253  if (i > k) { break; }
4254  const nki = await mcp.call('calculator', 'add', { a: nmk, b: i });
4255  const num = await mcp.call('calculator', 'multiply', { a: result.result, b: nki.result });
4256  result = await mcp.call('calculator', 'divide', { a: num.result, b: i });
4257}
4258return { n: args.n, k: args.k, result: result.result };
4259            "#;
4260
4261            let mut compiler = PlanCompiler::new();
4262            let plan = compiler.compile_code(code).expect("Should compile");
4263
4264            let mock_http = MockHttpExecutor::new();
4265            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4266            executor.set_mcp_executor(MockCalculatorExecutor);
4267            executor.set_variable("args", serde_json::json!({"n": 5, "k": 2}));
4268
4269            let result = executor.execute(&plan).await.expect("Should execute");
4270            assert_eq!(
4271                result.value["result"], 10.0,
4272                "C(5,2) should be 10, got: {:?}",
4273                result.value
4274            );
4275        }
4276
4277        #[tokio::test]
4278        async fn test_combinations_edge_cases() {
4279            let code = r#"
4280if (args.k === 0 || args.k === args.n) {
4281  return { result: 1 };
4282}
4283return { result: 'not edge case' };
4284            "#;
4285
4286            let mut compiler = PlanCompiler::new();
4287            let plan = compiler.compile_code(code).expect("Should compile");
4288
4289            // Test k=0
4290            let mock_http = MockHttpExecutor::new();
4291            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4292            executor.set_variable("args", serde_json::json!({"n": 5, "k": 0}));
4293            let result = executor.execute(&plan).await.expect("Should execute");
4294            assert_eq!(result.value["result"], 1, "C(5,0) should be 1");
4295
4296            // Test k=n
4297            let mock_http = MockHttpExecutor::new();
4298            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4299            executor.set_variable("args", serde_json::json!({"n": 5, "k": 5}));
4300            let result = executor.execute(&plan).await.expect("Should execute");
4301            assert_eq!(result.value["result"], 1, "C(5,5) should be 1");
4302        }
4303
4304        #[tokio::test]
4305        async fn test_solve_quadratic() {
4306            // x² - 3x + 2 = 0 → roots [2, 1]
4307            let code = r#"
4308const b_sq = await mcp.call('calculator', 'power', { base: args.b, exponent: 2 });
4309const four_a = await mcp.call('calculator', 'multiply', { a: 4, b: args.a });
4310const four_ac = await mcp.call('calculator', 'multiply', { a: four_a.result, b: args.c });
4311const discriminant = await mcp.call('calculator', 'subtract', { a: b_sq.result, b: four_ac.result });
4312const root_type = discriminant.result > 0 ? 'two_real'
4313  : discriminant.result === 0 ? 'one_real' : 'complex';
4314if (discriminant.result < 0) {
4315  return { discriminant: discriminant.result, root_type: root_type, roots: [] };
4316}
4317const sqrt_disc = await mcp.call('calculator', 'sqrt', { n: discriminant.result });
4318const neg_b = await mcp.call('calculator', 'multiply', { a: -1, b: args.b });
4319const two_a = await mcp.call('calculator', 'multiply', { a: 2, b: args.a });
4320const x1_num = await mcp.call('calculator', 'add', { a: neg_b.result, b: sqrt_disc.result });
4321const x2_num = await mcp.call('calculator', 'subtract', { a: neg_b.result, b: sqrt_disc.result });
4322const x1 = await mcp.call('calculator', 'divide', { a: x1_num.result, b: two_a.result });
4323const x2 = await mcp.call('calculator', 'divide', { a: x2_num.result, b: two_a.result });
4324return { discriminant: discriminant.result, root_type: root_type, roots: [x1.result, x2.result] };
4325            "#;
4326
4327            let mut compiler = PlanCompiler::new();
4328            let plan = compiler.compile_code(code).expect("Should compile");
4329
4330            let mock_http = MockHttpExecutor::new();
4331            let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4332            executor.set_mcp_executor(MockCalculatorExecutor);
4333            executor.set_variable("args", serde_json::json!({"a": 1, "b": -3, "c": 2}));
4334
4335            let result = executor.execute(&plan).await.expect("Should execute");
4336            assert_eq!(result.value["root_type"], "two_real");
4337            assert_eq!(result.value["discriminant"], 1.0);
4338            let roots = result.value["roots"]
4339                .as_array()
4340                .expect("roots should be array");
4341            assert_eq!(roots.len(), 2);
4342            assert_eq!(roots[0], 2.0);
4343            assert_eq!(roots[1], 1.0);
4344        }
4345    }
4346
4347    // =========================================================================
4348    // String method integration tests (compile + execute)
4349    // =========================================================================
4350
4351    #[tokio::test]
4352    async fn test_string_includes() {
4353        let code = r#"
4354            const text = "hello world";
4355            return { found: text.includes("world"), miss: text.includes("xyz") };
4356        "#;
4357
4358        let mut compiler = PlanCompiler::new();
4359        let plan = compiler.compile_code(code).expect("Should compile");
4360
4361        let mock_http = MockHttpExecutor::new();
4362        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4363        let result = executor.execute(&plan).await.expect("Should execute");
4364
4365        assert_eq!(result.value["found"], true);
4366        assert_eq!(result.value["miss"], false);
4367    }
4368
4369    #[tokio::test]
4370    async fn test_string_index_of() {
4371        let code = r#"
4372            const text = "abcdef";
4373            return { idx: text.indexOf("cd"), miss: text.indexOf("xyz") };
4374        "#;
4375
4376        let mut compiler = PlanCompiler::new();
4377        let plan = compiler.compile_code(code).expect("Should compile");
4378
4379        let mock_http = MockHttpExecutor::new();
4380        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4381        let result = executor.execute(&plan).await.expect("Should execute");
4382
4383        assert_eq!(result.value["idx"], 2);
4384        assert_eq!(result.value["miss"], -1);
4385    }
4386
4387    #[tokio::test]
4388    async fn test_string_length() {
4389        let code = r#"
4390            const text = "hello";
4391            return { len: text.length };
4392        "#;
4393
4394        let mut compiler = PlanCompiler::new();
4395        let plan = compiler.compile_code(code).expect("Should compile");
4396
4397        let mock_http = MockHttpExecutor::new();
4398        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4399        let result = executor.execute(&plan).await.expect("Should execute");
4400
4401        assert_eq!(result.value["len"], 5);
4402    }
4403
4404    #[tokio::test]
4405    async fn test_string_slice() {
4406        let code = r#"
4407            const text = "hello world";
4408            return { first: text.slice(0, 5), rest: text.slice(6, 11) };
4409        "#;
4410
4411        let mut compiler = PlanCompiler::new();
4412        let plan = compiler.compile_code(code).expect("Should compile");
4413
4414        let mock_http = MockHttpExecutor::new();
4415        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4416        let result = executor.execute(&plan).await.expect("Should execute");
4417
4418        assert_eq!(result.value["first"], "hello");
4419        assert_eq!(result.value["rest"], "world");
4420    }
4421
4422    #[tokio::test]
4423    async fn test_string_concat() {
4424        let code = r#"
4425            const greeting = "hello";
4426            return { result: greeting.concat(" world") };
4427        "#;
4428
4429        let mut compiler = PlanCompiler::new();
4430        let plan = compiler.compile_code(code).expect("Should compile");
4431
4432        let mock_http = MockHttpExecutor::new();
4433        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4434        let result = executor.execute(&plan).await.expect("Should execute");
4435
4436        assert_eq!(result.value["result"], "hello world");
4437    }
4438
4439    #[tokio::test]
4440    async fn test_string_includes_in_filter() {
4441        // Real-world pattern: filter array items by string content
4442        let code = r#"
4443            const items = [
4444                { name: "TIMESTAMP_2024", desc: "A timestamped record" },
4445                { name: "PERSON_1", desc: "A person entity" },
4446                { name: "TIMESTAMP_2025", desc: "Another timestamped record" }
4447            ];
4448            const timestamped = items.filter(item => item.name.includes("TIMESTAMP"));
4449            return { count: timestamped.length, names: timestamped.map(t => t.name) };
4450        "#;
4451
4452        let mut compiler = PlanCompiler::new();
4453        let plan = compiler.compile_code(code).expect("Should compile");
4454
4455        let mock_http = MockHttpExecutor::new();
4456        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4457        let result = executor.execute(&plan).await.expect("Should execute");
4458
4459        assert_eq!(result.value["count"], 2);
4460        let names = result.value["names"].as_array().unwrap();
4461        assert_eq!(names[0], "TIMESTAMP_2024");
4462        assert_eq!(names[1], "TIMESTAMP_2025");
4463    }
4464
4465    #[tokio::test]
4466    async fn test_array_includes_still_works() {
4467        // Regression: array .includes() must still work
4468        let code = r#"
4469            const ids = ["alice", "bob", "charlie"];
4470            return { has_bob: ids.includes("bob"), has_dave: ids.includes("dave") };
4471        "#;
4472
4473        let mut compiler = PlanCompiler::new();
4474        let plan = compiler.compile_code(code).expect("Should compile");
4475
4476        let mock_http = MockHttpExecutor::new();
4477        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4478        let result = executor.execute(&plan).await.expect("Should execute");
4479
4480        assert_eq!(result.value["has_bob"], true);
4481        assert_eq!(result.value["has_dave"], false);
4482    }
4483
4484    // =========================================================================
4485    // Built-in function compilation tests
4486    // =========================================================================
4487
4488    #[test]
4489    fn test_compile_parse_float() {
4490        let code = r#"
4491            const x = parseFloat("3.14");
4492            return x;
4493        "#;
4494        let mut compiler = PlanCompiler::new();
4495        let plan = compiler
4496            .compile_code(code)
4497            .expect("parseFloat should compile");
4498        assert_eq!(plan.steps.len(), 2); // Assign + Return
4499    }
4500
4501    #[test]
4502    fn test_compile_parse_int() {
4503        let code = r#"
4504            const x = parseInt("42");
4505            return x;
4506        "#;
4507        let mut compiler = PlanCompiler::new();
4508        compiler
4509            .compile_code(code)
4510            .expect("parseInt should compile");
4511    }
4512
4513    #[test]
4514    fn test_compile_math_abs() {
4515        let code = r#"
4516            const x = Math.abs(-5);
4517            return x;
4518        "#;
4519        let mut compiler = PlanCompiler::new();
4520        compiler
4521            .compile_code(code)
4522            .expect("Math.abs should compile");
4523    }
4524
4525    #[test]
4526    fn test_compile_math_max() {
4527        let code = r#"
4528            const x = Math.max(1, 2, 3);
4529            return x;
4530        "#;
4531        let mut compiler = PlanCompiler::new();
4532        compiler
4533            .compile_code(code)
4534            .expect("Math.max should compile");
4535    }
4536
4537    #[test]
4538    fn test_compile_object_keys() {
4539        let code = r#"
4540            const obj = { a: 1, b: 2 };
4541            const keys = Object.keys(obj);
4542            return keys;
4543        "#;
4544        let mut compiler = PlanCompiler::new();
4545        compiler
4546            .compile_code(code)
4547            .expect("Object.keys should compile");
4548    }
4549
4550    #[test]
4551    fn test_compile_object_entries() {
4552        let code = r#"
4553            const obj = { x: 10 };
4554            const entries = Object.entries(obj);
4555            return entries;
4556        "#;
4557        let mut compiler = PlanCompiler::new();
4558        compiler
4559            .compile_code(code)
4560            .expect("Object.entries should compile");
4561    }
4562
4563    #[test]
4564    fn test_compile_unary_plus() {
4565        let code = r#"
4566            const x = +"42";
4567            return x;
4568        "#;
4569        let mut compiler = PlanCompiler::new();
4570        compiler.compile_code(code).expect("unary + should compile");
4571    }
4572
4573    #[test]
4574    fn test_compile_sort_with_comparator() {
4575        let code = r#"
4576            const arr = [3, 1, 2];
4577            const sorted = arr.sort((a, b) => a - b);
4578            return sorted;
4579        "#;
4580        let mut compiler = PlanCompiler::new();
4581        compiler
4582            .compile_code(code)
4583            .expect("sort with comparator should compile");
4584    }
4585
4586    #[test]
4587    fn test_compile_sort_without_comparator() {
4588        let code = r#"
4589            const arr = ["b", "a", "c"];
4590            const sorted = arr.sort();
4591            return sorted;
4592        "#;
4593        let mut compiler = PlanCompiler::new();
4594        compiler
4595            .compile_code(code)
4596            .expect("sort without comparator should compile");
4597    }
4598
4599    // =========================================================================
4600    // End-to-end execution tests for new features
4601    // =========================================================================
4602
4603    #[tokio::test]
4604    async fn test_execute_parse_float() {
4605        let code = r#"
4606            const x = parseFloat("3.14");
4607            return x;
4608        "#;
4609        let mut compiler = PlanCompiler::new();
4610        let plan = compiler.compile_code(code).unwrap();
4611        let mock_http = MockHttpExecutor::new();
4612        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4613        let result = executor.execute(&plan).await.unwrap();
4614        assert_eq!(result.value, serde_json::json!(3.14));
4615    }
4616
4617    #[tokio::test]
4618    async fn test_execute_math_abs_and_sort() {
4619        let code = r#"
4620            const items = [
4621                { name: "a", val: -5 },
4622                { name: "b", val: 3 },
4623                { name: "c", val: -1 }
4624            ];
4625            const sorted = items.sort((a, b) => Math.abs(b.val) - Math.abs(a.val));
4626            return sorted.map(x => x.name);
4627        "#;
4628        let mut compiler = PlanCompiler::new();
4629        let plan = compiler.compile_code(code).unwrap();
4630        let mock_http = MockHttpExecutor::new();
4631        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4632        let result = executor.execute(&plan).await.unwrap();
4633        assert_eq!(result.value, serde_json::json!(["a", "b", "c"]));
4634    }
4635
4636    #[tokio::test]
4637    async fn test_execute_object_keys() {
4638        let code = r#"
4639            const obj = { x: 1, y: 2, z: 3 };
4640            return Object.keys(obj).length;
4641        "#;
4642        let mut compiler = PlanCompiler::new();
4643        let plan = compiler.compile_code(code).unwrap();
4644        let mock_http = MockHttpExecutor::new();
4645        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4646        let result = executor.execute(&plan).await.unwrap();
4647        assert_eq!(result.value, serde_json::json!(3));
4648    }
4649
4650    #[tokio::test]
4651    async fn test_execute_unary_plus() {
4652        let code = r#"
4653            const x = +"42";
4654            return x;
4655        "#;
4656        let mut compiler = PlanCompiler::new();
4657        let plan = compiler.compile_code(code).unwrap();
4658        let mock_http = MockHttpExecutor::new();
4659        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4660        let result = executor.execute(&plan).await.unwrap();
4661        assert_eq!(result.value, serde_json::json!(42.0));
4662    }
4663
4664    #[tokio::test]
4665    async fn test_execute_number_cast() {
4666        let code = r#"
4667            const x = Number("99.5");
4668            return x;
4669        "#;
4670        let mut compiler = PlanCompiler::new();
4671        let plan = compiler.compile_code(code).unwrap();
4672        let mock_http = MockHttpExecutor::new();
4673        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4674        let result = executor.execute(&plan).await.unwrap();
4675        assert_eq!(result.value, serde_json::json!(99.5));
4676    }
4677
4678    #[tokio::test]
4679    async fn test_execute_math_round_floor_ceil() {
4680        let code = r#"
4681            return {
4682                round: Math.round(3.7),
4683                floor: Math.floor(3.7),
4684                ceil: Math.ceil(3.2)
4685            };
4686        "#;
4687        let mut compiler = PlanCompiler::new();
4688        let plan = compiler.compile_code(code).unwrap();
4689        let mock_http = MockHttpExecutor::new();
4690        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4691        let result = executor.execute(&plan).await.unwrap();
4692        assert_eq!(result.value["round"], serde_json::json!(4.0));
4693        assert_eq!(result.value["floor"], serde_json::json!(3.0));
4694        assert_eq!(result.value["ceil"], serde_json::json!(4.0));
4695    }
4696
4697    // =========================================================================
4698    // Object Spread Tests
4699    // =========================================================================
4700
4701    #[test]
4702    fn test_compile_object_spread_basic() {
4703        let code = r#"
4704            const base = { id: 1, name: "Alice" };
4705            const extended = { ...base, age: 30 };
4706            return extended;
4707        "#;
4708        let mut compiler = PlanCompiler::new();
4709        let plan = compiler
4710            .compile_code(code)
4711            .expect("Object spread should compile");
4712        assert!(plan.steps.len() >= 2);
4713    }
4714
4715    #[tokio::test]
4716    async fn test_execute_object_spread_basic() {
4717        let code = r#"
4718            const base = { id: 1, name: "Alice" };
4719            const extended = { ...base, age: 30 };
4720            return extended;
4721        "#;
4722        let mut compiler = PlanCompiler::new();
4723        let plan = compiler.compile_code(code).unwrap();
4724        let mock_http = MockHttpExecutor::new();
4725        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4726        let result = executor.execute(&plan).await.unwrap();
4727        assert_eq!(result.value["id"], serde_json::json!(1));
4728        assert_eq!(result.value["name"], serde_json::json!("Alice"));
4729        assert_eq!(result.value["age"], serde_json::json!(30));
4730    }
4731
4732    #[tokio::test]
4733    async fn test_execute_object_spread_override() {
4734        // Later properties should override spread properties (JS semantics)
4735        let code = r#"
4736            const obj = { id: 1, name: "old" };
4737            const updated = { ...obj, name: "new" };
4738            return updated;
4739        "#;
4740        let mut compiler = PlanCompiler::new();
4741        let plan = compiler.compile_code(code).unwrap();
4742        let mock_http = MockHttpExecutor::new();
4743        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4744        let result = executor.execute(&plan).await.unwrap();
4745        assert_eq!(result.value["id"], serde_json::json!(1));
4746        assert_eq!(result.value["name"], serde_json::json!("new"));
4747    }
4748
4749    #[tokio::test]
4750    async fn test_execute_object_spread_multiple() {
4751        let code = r#"
4752            const a = { x: 1 };
4753            const b = { y: 2 };
4754            const merged = { ...a, ...b, z: 3 };
4755            return merged;
4756        "#;
4757        let mut compiler = PlanCompiler::new();
4758        let plan = compiler.compile_code(code).unwrap();
4759        let mock_http = MockHttpExecutor::new();
4760        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4761        let result = executor.execute(&plan).await.unwrap();
4762        assert_eq!(result.value["x"], serde_json::json!(1));
4763        assert_eq!(result.value["y"], serde_json::json!(2));
4764        assert_eq!(result.value["z"], serde_json::json!(3));
4765    }
4766
4767    #[tokio::test]
4768    async fn test_execute_object_spread_with_api_result() {
4769        // Primary use case: spread API result into a new object
4770        let code = r#"
4771            const config = await api.get('/config');
4772            const result = { ...config, extra: "added" };
4773            return result;
4774        "#;
4775        let mut compiler = PlanCompiler::new();
4776        let plan = compiler.compile_code(code).unwrap();
4777        let mut mock_http = MockHttpExecutor::new();
4778        mock_http.add_response(
4779            "/config",
4780            serde_json::json!({ "key": "value", "enabled": true }),
4781        );
4782        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4783        let result = executor.execute(&plan).await.unwrap();
4784        assert_eq!(result.value["key"], serde_json::json!("value"));
4785        assert_eq!(result.value["enabled"], serde_json::json!(true));
4786        assert_eq!(result.value["extra"], serde_json::json!("added"));
4787    }
4788
4789    #[tokio::test]
4790    async fn test_execute_object_spread_non_object_noop() {
4791        // Spreading a non-object should be a no-op (matches JS behavior)
4792        let code = r#"
4793            const x = 42;
4794            const obj = { ...x, name: "test" };
4795            return obj;
4796        "#;
4797        let mut compiler = PlanCompiler::new();
4798        let plan = compiler.compile_code(code).unwrap();
4799        let mock_http = MockHttpExecutor::new();
4800        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4801        let result = executor.execute(&plan).await.unwrap();
4802        assert_eq!(result.value["name"], serde_json::json!("test"));
4803        // x (number) should not add any properties
4804        assert!(result.value.as_object().unwrap().len() == 1);
4805    }
4806
4807    #[tokio::test]
4808    async fn test_execute_object_spread_preserves_order() {
4809        // Spread before explicit property: explicit wins
4810        // Explicit before spread: spread wins
4811        let code = r#"
4812            const obj = { a: 1, b: 2 };
4813            const result = { b: 99, ...obj, a: 100 };
4814            return result;
4815        "#;
4816        let mut compiler = PlanCompiler::new();
4817        let plan = compiler.compile_code(code).unwrap();
4818        let mock_http = MockHttpExecutor::new();
4819        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4820        let result = executor.execute(&plan).await.unwrap();
4821        // { b: 99 } then { ...obj } → b overridden to 2, then { a: 100 } → a overridden to 100
4822        assert_eq!(result.value["a"], serde_json::json!(100));
4823        assert_eq!(result.value["b"], serde_json::json!(2));
4824    }
4825
4826    // =========================================================================
4827    // Object Destructuring Tests
4828    // =========================================================================
4829
4830    #[test]
4831    fn test_compile_object_destructuring_simple() {
4832        let code = r#"
4833            const obj = { id: 1, name: "Alice" };
4834            const { id, name } = obj;
4835            return { id, name };
4836        "#;
4837        let mut compiler = PlanCompiler::new();
4838        let plan = compiler
4839            .compile_code(code)
4840            .expect("Object destructuring should compile");
4841        // Should have: Assign(obj), Assign(__destructure_0), Assign(id), Assign(name), Return
4842        assert!(plan.steps.len() >= 4);
4843    }
4844
4845    #[tokio::test]
4846    async fn test_execute_object_destructuring_simple() {
4847        let code = r#"
4848            const obj = { id: 1, name: "Alice", extra: "ignored" };
4849            const { id, name } = obj;
4850            return { id, name };
4851        "#;
4852        let mut compiler = PlanCompiler::new();
4853        let plan = compiler.compile_code(code).unwrap();
4854        let mock_http = MockHttpExecutor::new();
4855        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4856        let result = executor.execute(&plan).await.unwrap();
4857        assert_eq!(result.value["id"], serde_json::json!(1));
4858        assert_eq!(result.value["name"], serde_json::json!("Alice"));
4859        // "extra" should not be in output since we only destructured id and name
4860        assert!(result.value.get("extra").is_none());
4861    }
4862
4863    #[tokio::test]
4864    async fn test_execute_object_destructuring_renamed() {
4865        let code = r#"
4866            const user = { id: 1, name: "Alice" };
4867            const { id: userId, name: userName } = user;
4868            return { userId, userName };
4869        "#;
4870        let mut compiler = PlanCompiler::new();
4871        let plan = compiler.compile_code(code).unwrap();
4872        let mock_http = MockHttpExecutor::new();
4873        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4874        let result = executor.execute(&plan).await.unwrap();
4875        assert_eq!(result.value["userId"], serde_json::json!(1));
4876        assert_eq!(result.value["userName"], serde_json::json!("Alice"));
4877    }
4878
4879    #[tokio::test]
4880    async fn test_execute_object_destructuring_with_api_call() {
4881        // The primary use case: destructure API response
4882        let code = r#"
4883            const { data, status } = await api.get('/users');
4884            return { data, status };
4885        "#;
4886        let mut compiler = PlanCompiler::new();
4887        let plan = compiler.compile_code(code).unwrap();
4888        let mut mock_http = MockHttpExecutor::new();
4889        mock_http.add_response(
4890            "/users",
4891            serde_json::json!({ "data": [{"id": 1}], "status": "ok", "meta": "hidden" }),
4892        );
4893        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4894        let result = executor.execute(&plan).await.unwrap();
4895        assert_eq!(result.value["data"], serde_json::json!([{"id": 1}]));
4896        assert_eq!(result.value["status"], serde_json::json!("ok"));
4897    }
4898
4899    #[tokio::test]
4900    async fn test_execute_object_destructuring_missing_property() {
4901        // Missing properties should be null (matches JS behavior)
4902        let code = r#"
4903            const obj = { id: 1 };
4904            const { id, name } = obj;
4905            return { id, name };
4906        "#;
4907        let mut compiler = PlanCompiler::new();
4908        let plan = compiler.compile_code(code).unwrap();
4909        let mock_http = MockHttpExecutor::new();
4910        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4911        let result = executor.execute(&plan).await.unwrap();
4912        assert_eq!(result.value["id"], serde_json::json!(1));
4913        assert_eq!(result.value["name"], serde_json::json!(null));
4914    }
4915
4916    // =========================================================================
4917    // Array Destructuring Tests
4918    // =========================================================================
4919
4920    #[tokio::test]
4921    async fn test_execute_array_destructuring_simple() {
4922        let code = r#"
4923            const arr = [10, 20, 30];
4924            const [a, b] = arr;
4925            return { a, b };
4926        "#;
4927        let mut compiler = PlanCompiler::new();
4928        let plan = compiler.compile_code(code).unwrap();
4929        let mock_http = MockHttpExecutor::new();
4930        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4931        let result = executor.execute(&plan).await.unwrap();
4932        assert_eq!(result.value["a"], serde_json::json!(10));
4933        assert_eq!(result.value["b"], serde_json::json!(20));
4934    }
4935
4936    #[tokio::test]
4937    async fn test_execute_array_destructuring_with_promise_all() {
4938        // Common pattern: destructure Promise.all results
4939        let code = r#"
4940            const [users, products] = await Promise.all([
4941                api.get('/users'),
4942                api.get('/products')
4943            ]);
4944            return { users, products };
4945        "#;
4946        let mut compiler = PlanCompiler::new();
4947        let plan = compiler.compile_code(code).unwrap();
4948        let mut mock_http = MockHttpExecutor::new();
4949        mock_http.add_response("/users", serde_json::json!([{"id": 1}]));
4950        mock_http.add_response("/products", serde_json::json!([{"sku": "A"}]));
4951        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4952        let result = executor.execute(&plan).await.unwrap();
4953        assert_eq!(result.value["users"], serde_json::json!([{"id": 1}]));
4954        assert_eq!(result.value["products"], serde_json::json!([{"sku": "A"}]));
4955    }
4956
4957    // =========================================================================
4958    // For-of Loop Destructuring Tests
4959    // =========================================================================
4960
4961    #[test]
4962    fn test_compile_for_of_destructuring() {
4963        let code = r#"
4964            const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4965            const results = [];
4966            for (const { id, name } of items.slice(0, 10)) {
4967                results.push({ id, name });
4968            }
4969            return results;
4970        "#;
4971        let mut compiler = PlanCompiler::new();
4972        compiler
4973            .compile_code(code)
4974            .expect("For-of with destructuring should compile");
4975    }
4976
4977    #[tokio::test]
4978    async fn test_execute_for_of_destructuring() {
4979        let code = r#"
4980            const items = [{ id: 1, name: "A" }, { id: 2, name: "B" }];
4981            const results = [];
4982            for (const { id, name } of items.slice(0, 10)) {
4983                results.push({ label: name, num: id });
4984            }
4985            return results;
4986        "#;
4987        let mut compiler = PlanCompiler::new();
4988        let plan = compiler.compile_code(code).unwrap();
4989        let mock_http = MockHttpExecutor::new();
4990        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
4991        let result = executor.execute(&plan).await.unwrap();
4992        let arr = result.value.as_array().unwrap();
4993        assert_eq!(arr.len(), 2);
4994        assert_eq!(arr[0]["label"], serde_json::json!("A"));
4995        assert_eq!(arr[0]["num"], serde_json::json!(1));
4996        assert_eq!(arr[1]["label"], serde_json::json!("B"));
4997        assert_eq!(arr[1]["num"], serde_json::json!(2));
4998    }
4999
5000    #[tokio::test]
5001    async fn test_execute_for_of_destructuring_with_api_calls() {
5002        // Real-world pattern: destructure loop items, use properties in API calls
5003        let code = r#"
5004            const users = [{ id: 1, role: "admin" }, { id: 2, role: "user" }];
5005            const results = [];
5006            for (const { id, role } of users.slice(0, 10)) {
5007                const detail = await api.get(`/users/${id}`);
5008                results.push({ role, detail });
5009            }
5010            return results;
5011        "#;
5012        let mut compiler = PlanCompiler::new();
5013        let plan = compiler.compile_code(code).unwrap();
5014        let mut mock_http = MockHttpExecutor::new();
5015        mock_http.add_response("/users/1", serde_json::json!({ "name": "Alice" }));
5016        mock_http.add_response("/users/2", serde_json::json!({ "name": "Bob" }));
5017        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5018        let result = executor.execute(&plan).await.unwrap();
5019        let arr = result.value.as_array().unwrap();
5020        assert_eq!(arr.len(), 2);
5021        assert_eq!(arr[0]["role"], serde_json::json!("admin"));
5022        assert_eq!(arr[0]["detail"]["name"], serde_json::json!("Alice"));
5023        assert_eq!(arr[1]["role"], serde_json::json!("user"));
5024        assert_eq!(arr[1]["detail"]["name"], serde_json::json!("Bob"));
5025    }
5026
5027    // =========================================================================
5028    // Combined Spread + Destructuring Tests
5029    // =========================================================================
5030
5031    #[tokio::test]
5032    async fn test_execute_spread_and_destructuring_combined() {
5033        // Realistic pattern: destructure API response, spread into new request
5034        let code = r#"
5035            const { data, token } = await api.get('/auth');
5036            const result = await api.post('/action', { ...data, token });
5037            return result;
5038        "#;
5039        let mut compiler = PlanCompiler::new();
5040        let plan = compiler.compile_code(code).unwrap();
5041        let mut mock_http = MockHttpExecutor::new();
5042        mock_http.add_response(
5043            "/auth",
5044            serde_json::json!({ "data": { "user": "alice" }, "token": "abc123" }),
5045        );
5046        mock_http.add_response("/action", serde_json::json!({ "success": true }));
5047        let mut executor = PlanExecutor::new(mock_http, ExecutionConfig::default());
5048        let result = executor.execute(&plan).await.unwrap();
5049        assert_eq!(result.value["success"], serde_json::json!(true));
5050    }
5051}