Skip to main content

tl_compiler/
vm.rs

1// ThinkingLanguage — Bytecode Virtual Machine
2// Register-based VM that executes compiled bytecode.
3
4use std::collections::HashMap;
5#[cfg(feature = "native")]
6use std::sync::mpsc;
7use std::sync::{Arc, Mutex, OnceLock};
8
9/// Global mutex for env_set/env_remove thread safety (std::env::set_var is not thread-safe).
10static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11fn env_lock() -> std::sync::MutexGuard<'static, ()> {
12    ENV_MUTEX
13        .get_or_init(|| Mutex::new(()))
14        .lock()
15        .unwrap_or_else(|e| e.into_inner())
16}
17#[cfg(feature = "native")]
18use std::time::Duration;
19
20#[cfg(feature = "native")]
21use rayon::prelude::*;
22use tl_ast::Expr as AstExpr;
23#[cfg(feature = "native")]
24use tl_data::datafusion::execution::FunctionRegistry;
25#[cfg(feature = "native")]
26use tl_data::translate::{LocalValue, TranslateContext, translate_expr};
27#[cfg(feature = "native")]
28use tl_data::{DataEngine, JoinType, col, lit};
29use tl_errors::{RuntimeError, TlError};
30
31use crate::chunk::*;
32use crate::opcode::*;
33use crate::value::*;
34
35fn decimal_to_f64(d: &rust_decimal::Decimal) -> f64 {
36    use rust_decimal::prelude::ToPrimitive;
37    d.to_f64().unwrap_or(f64::NAN)
38}
39
40fn runtime_err(msg: impl Into<String>) -> TlError {
41    TlError::Runtime(RuntimeError {
42        message: msg.into(),
43        span: None,
44        stack_trace: vec![],
45    })
46}
47
48/// Resolve a connection name via TL_CONFIG_PATH config file.
49/// If `name` looks like a connection string (contains `=` or `://`), return it as-is.
50/// Otherwise, look it up in the JSON config file at `TL_CONFIG_PATH` (or `./tl_config.json`).
51fn resolve_tl_config_connection(name: &str) -> String {
52    // If it already looks like a connection string, pass through
53    if name.contains('=') || name.contains("://") {
54        return name.to_string();
55    }
56    // Try to load config
57    let config_path =
58        std::env::var("TL_CONFIG_PATH").unwrap_or_else(|_| "tl_config.json".to_string());
59    let Ok(contents) = std::fs::read_to_string(&config_path) else {
60        return name.to_string();
61    };
62    let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
63        return name.to_string();
64    };
65    // Look up in "connections" object first, then top-level
66    if let Some(conn) = json
67        .get("connections")
68        .and_then(|c| c.get(name))
69        .and_then(|v| v.as_str())
70    {
71        return conn.to_string();
72    }
73    if let Some(conn) = json.get(name).and_then(|v| v.as_str()) {
74        return conn.to_string();
75    }
76    // Not found — return original (will fail at connection time with a clear error)
77    name.to_string()
78}
79
80/// Compare two VmValues for equality (used by set operations).
81fn vm_values_equal(a: &VmValue, b: &VmValue) -> bool {
82    match (a, b) {
83        (VmValue::Int(x), VmValue::Int(y)) => x == y,
84        (VmValue::Float(x), VmValue::Float(y)) => x == y,
85        (VmValue::String(x), VmValue::String(y)) => x == y,
86        (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
87        (VmValue::None, VmValue::None) => true,
88        _ => false,
89    }
90}
91
92#[cfg(feature = "native")]
93/// Resolve a file path within a package directory for package imports.
94/// `pkg_root` is the package root (containing tl.toml).
95/// `remaining` are the path segments after the package name.
96/// Entry point convention: src/lib.tl > src/mod.tl > src/main.tl > mod.tl > lib.tl
97fn resolve_package_file(pkg_root: &std::path::Path, remaining: &[&str]) -> Option<String> {
98    if remaining.is_empty() {
99        // Import the package itself — find entry point
100        let src = pkg_root.join("src");
101        for entry in &["lib.tl", "mod.tl", "main.tl"] {
102            let p = src.join(entry);
103            if p.exists() {
104                return Some(p.to_string_lossy().to_string());
105            }
106        }
107        for entry in &["mod.tl", "lib.tl"] {
108            let p = pkg_root.join(entry);
109            if p.exists() {
110                return Some(p.to_string_lossy().to_string());
111            }
112        }
113        return None;
114    }
115
116    // Try src/<remaining>.tl, then src/<remaining>/mod.tl
117    let rel = remaining.join("/");
118    let src = pkg_root.join("src");
119
120    let file_path = src.join(format!("{rel}.tl"));
121    if file_path.exists() {
122        return Some(file_path.to_string_lossy().to_string());
123    }
124
125    let dir_path = src.join(&rel).join("mod.tl");
126    if dir_path.exists() {
127        return Some(dir_path.to_string_lossy().to_string());
128    }
129
130    // Also try without src/ prefix
131    let file_path = pkg_root.join(format!("{rel}.tl"));
132    if file_path.exists() {
133        return Some(file_path.to_string_lossy().to_string());
134    }
135
136    let dir_path = pkg_root.join(&rel).join("mod.tl");
137    if dir_path.exists() {
138        return Some(dir_path.to_string_lossy().to_string());
139    }
140
141    // Parent fallback for item-within-module
142    if remaining.len() > 1 {
143        let parent = &remaining[..remaining.len() - 1];
144        let parent_rel = parent.join("/");
145        let parent_file = src.join(format!("{parent_rel}.tl"));
146        if parent_file.exists() {
147            return Some(parent_file.to_string_lossy().to_string());
148        }
149        let parent_file = pkg_root.join(format!("{parent_rel}.tl"));
150        if parent_file.exists() {
151            return Some(parent_file.to_string_lossy().to_string());
152        }
153    }
154
155    None
156}
157
158/// Convert serde_json::Value to VmValue
159fn vm_json_to_value(v: &serde_json::Value) -> VmValue {
160    match v {
161        serde_json::Value::Null => VmValue::None,
162        serde_json::Value::Bool(b) => VmValue::Bool(*b),
163        serde_json::Value::Number(n) => {
164            if let Some(i) = n.as_i64() {
165                VmValue::Int(i)
166            } else {
167                VmValue::Float(n.as_f64().unwrap_or(0.0))
168            }
169        }
170        serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
171        serde_json::Value::Array(arr) => {
172            VmValue::List(Box::new(arr.iter().map(vm_json_to_value).collect()))
173        }
174        serde_json::Value::Object(obj) => VmValue::Map(Box::new(
175            obj.iter()
176                .map(|(k, v)| (Arc::from(k.as_str()), vm_json_to_value(v)))
177                .collect(),
178        )),
179    }
180}
181
182/// Convert VmValue to serde_json::Value
183fn vm_value_to_json(v: &VmValue) -> serde_json::Value {
184    match v {
185        VmValue::None => serde_json::Value::Null,
186        VmValue::Bool(b) => serde_json::Value::Bool(*b),
187        VmValue::Int(n) => serde_json::json!(*n),
188        VmValue::Float(n) => serde_json::json!(*n),
189        VmValue::String(s) => serde_json::Value::String(s.to_string()),
190        VmValue::List(items) => {
191            serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
192        }
193        VmValue::Map(pairs) => {
194            let obj: serde_json::Map<String, serde_json::Value> = pairs
195                .iter()
196                .map(|(k, v)| (k.to_string(), vm_value_to_json(v)))
197                .collect();
198            serde_json::Value::Object(obj)
199        }
200        VmValue::Secret(_) => serde_json::Value::String("***".to_string()),
201        _ => serde_json::Value::String(format!("{v}")),
202    }
203}
204
205/// Minimum list size before we attempt parallel execution.
206#[cfg(feature = "native")]
207const PARALLEL_THRESHOLD: usize = 10_000;
208
209/// Check if a closure is pure (no captured upvalues) and thus safe to run in parallel.
210#[cfg(feature = "native")]
211fn is_pure_closure(func: &VmValue) -> bool {
212    match func {
213        VmValue::Function(closure) => closure.upvalues.is_empty(),
214        _ => false,
215    }
216}
217
218/// Execute a pure function (no upvalues) in an isolated mini-VM.
219/// Used by rayon parallel operations — each thread gets its own stack.
220#[cfg(feature = "native")]
221fn execute_pure_fn(proto: &Arc<Prototype>, args: &[VmValue]) -> Result<VmValue, TlError> {
222    let base = 0;
223    let num_regs = proto.num_registers as usize;
224    let mut stack = vec![VmValue::None; num_regs + 1];
225    for (i, arg) in args.iter().enumerate() {
226        stack[i] = arg.clone();
227    }
228
229    let mut ip = 0;
230    loop {
231        if ip >= proto.code.len() {
232            return Ok(VmValue::None);
233        }
234        let inst = proto.code[ip];
235        let op = decode_op(inst);
236        let a = decode_a(inst);
237        let b = decode_b(inst);
238        let c = decode_c(inst);
239        let bx = decode_bx(inst);
240        let sbx = decode_sbx(inst);
241
242        ip += 1;
243
244        match op {
245            Op::LoadConst => {
246                let val = match &proto.constants[bx as usize] {
247                    Constant::Int(n) => VmValue::Int(*n),
248                    Constant::Float(n) => VmValue::Float(*n),
249                    Constant::String(s) => VmValue::String(s.clone()),
250                    Constant::Decimal(s) => {
251                        use std::str::FromStr;
252                        VmValue::Decimal(rust_decimal::Decimal::from_str(s).unwrap_or_default())
253                    }
254                    _ => VmValue::None,
255                };
256                stack[base + a as usize] = val;
257            }
258            Op::LoadNone => stack[base + a as usize] = VmValue::None,
259            Op::LoadTrue => stack[base + a as usize] = VmValue::Bool(true),
260            Op::LoadFalse => stack[base + a as usize] = VmValue::Bool(false),
261            Op::Move | Op::GetLocal => {
262                let val = stack[base + b as usize].clone();
263                stack[base + a as usize] = val;
264            }
265            Op::SetLocal => {
266                let val = stack[base + a as usize].clone();
267                stack[base + b as usize] = val;
268            }
269            Op::Add => {
270                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
271                    (VmValue::Int(x), VmValue::Int(y)) => x
272                        .checked_add(*y)
273                        .map(VmValue::Int)
274                        .unwrap_or_else(|| VmValue::Float(*x as f64 + *y as f64)),
275                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x + y),
276                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 + y),
277                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x + *y as f64),
278                    _ => return Err(runtime_err("Cannot add in parallel fn")),
279                };
280                stack[base + a as usize] = result;
281            }
282            Op::Sub => {
283                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
284                    (VmValue::Int(x), VmValue::Int(y)) => x
285                        .checked_sub(*y)
286                        .map(VmValue::Int)
287                        .unwrap_or_else(|| VmValue::Float(*x as f64 - *y as f64)),
288                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x - y),
289                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 - y),
290                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x - *y as f64),
291                    _ => return Err(runtime_err("Cannot subtract in parallel fn")),
292                };
293                stack[base + a as usize] = result;
294            }
295            Op::Mul => {
296                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
297                    (VmValue::Int(x), VmValue::Int(y)) => x
298                        .checked_mul(*y)
299                        .map(VmValue::Int)
300                        .unwrap_or_else(|| VmValue::Float(*x as f64 * *y as f64)),
301                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x * y),
302                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 * y),
303                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x * *y as f64),
304                    _ => return Err(runtime_err("Cannot multiply in parallel fn")),
305                };
306                stack[base + a as usize] = result;
307            }
308            Op::Div => {
309                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
310                    (VmValue::Int(x), VmValue::Int(y)) => {
311                        if *y == 0 {
312                            return Err(runtime_err("Division by zero"));
313                        }
314                        VmValue::Int(x / y)
315                    }
316                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x / y),
317                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 / y),
318                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x / *y as f64),
319                    _ => return Err(runtime_err("Cannot divide in parallel fn")),
320                };
321                stack[base + a as usize] = result;
322            }
323            Op::Mod => {
324                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
325                    (VmValue::Int(x), VmValue::Int(y)) => {
326                        if *y == 0 {
327                            return Err(runtime_err("Modulo by zero"));
328                        }
329                        VmValue::Int(x % y)
330                    }
331                    (VmValue::Float(x), VmValue::Float(y)) => {
332                        if *y == 0.0 {
333                            return Err(runtime_err("Modulo by zero"));
334                        }
335                        VmValue::Float(x % y)
336                    }
337                    _ => return Err(runtime_err("Cannot modulo in parallel fn")),
338                };
339                stack[base + a as usize] = result;
340            }
341            Op::Pow => {
342                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
343                    (VmValue::Int(x), VmValue::Int(y)) => {
344                        VmValue::Int((*x as f64).powi(*y as i32) as i64)
345                    }
346                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x.powf(*y)),
347                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float((*x as f64).powf(*y)),
348                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x.powi(*y as i32)),
349                    _ => return Err(runtime_err("Cannot pow in parallel fn")),
350                };
351                stack[base + a as usize] = result;
352            }
353            Op::Neg => {
354                let result = match &stack[base + b as usize] {
355                    VmValue::Int(n) => VmValue::Int(-n),
356                    VmValue::Float(n) => VmValue::Float(-n),
357                    _ => return Err(runtime_err("Cannot negate in parallel fn")),
358                };
359                stack[base + a as usize] = result;
360            }
361            Op::Eq => {
362                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
363                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
364                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
365                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
366                    (VmValue::String(x), VmValue::String(y)) => x == y,
367                    (VmValue::None, VmValue::None) => true,
368                    _ => false,
369                };
370                stack[base + a as usize] = VmValue::Bool(eq);
371            }
372            Op::Neq => {
373                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
374                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
375                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
376                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
377                    (VmValue::String(x), VmValue::String(y)) => x == y,
378                    (VmValue::None, VmValue::None) => true,
379                    _ => false,
380                };
381                stack[base + a as usize] = VmValue::Bool(!eq);
382            }
383            Op::Lt | Op::Gt | Op::Lte | Op::Gte => {
384                let cmp = match (&stack[base + b as usize], &stack[base + c as usize]) {
385                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y) as i8,
386                    (VmValue::Float(x), VmValue::Float(y)) => {
387                        if x < y {
388                            -1
389                        } else if x > y {
390                            1
391                        } else {
392                            0
393                        }
394                    }
395                    _ => return Err(runtime_err("Cannot compare in parallel fn")),
396                };
397                let result = match op {
398                    Op::Lt => cmp < 0,
399                    Op::Gt => cmp > 0,
400                    Op::Lte => cmp <= 0,
401                    Op::Gte => cmp >= 0,
402                    _ => unreachable!(),
403                };
404                stack[base + a as usize] = VmValue::Bool(result);
405            }
406            Op::And => {
407                let left = stack[base + b as usize].is_truthy();
408                let right = stack[base + c as usize].is_truthy();
409                stack[base + a as usize] = VmValue::Bool(left && right);
410            }
411            Op::Or => {
412                let left = stack[base + b as usize].is_truthy();
413                let right = stack[base + c as usize].is_truthy();
414                stack[base + a as usize] = VmValue::Bool(left || right);
415            }
416            Op::Not => {
417                let val = !stack[base + b as usize].is_truthy();
418                stack[base + a as usize] = VmValue::Bool(val);
419            }
420            Op::Jump => {
421                ip = (ip as i32 + sbx as i32) as usize;
422            }
423            Op::JumpIfFalse => {
424                if !stack[base + a as usize].is_truthy() {
425                    ip = (ip as i32 + sbx as i32) as usize;
426                }
427            }
428            Op::JumpIfTrue => {
429                if stack[base + a as usize].is_truthy() {
430                    ip = (ip as i32 + sbx as i32) as usize;
431                }
432            }
433            Op::Return => {
434                return Ok(stack[base + a as usize].clone());
435            }
436            // Unsupported ops in parallel context — fall back silently
437            _ => return Err(runtime_err("Unsupported op in parallel function")),
438        }
439    }
440}
441
442/// A call frame on the VM stack.
443struct CallFrame {
444    prototype: Arc<Prototype>,
445    ip: usize,
446    base: usize,
447    upvalues: Vec<UpvalueRef>,
448}
449
450/// A try-catch handler on the handler stack.
451struct TryHandler {
452    /// Frame index where try was entered
453    frame_idx: usize,
454    /// IP to jump to (catch handler)
455    catch_ip: usize,
456}
457
458/// The bytecode virtual machine.
459pub struct Vm {
460    /// Register stack
461    pub stack: Vec<VmValue>,
462    /// Call frame stack
463    frames: Vec<CallFrame>,
464    /// Global variables
465    pub globals: HashMap<String, VmValue>,
466    /// Data engine (lazily initialized)
467    #[cfg(feature = "native")]
468    data_engine: Option<DataEngine>,
469    /// Captured output (for testing)
470    pub output: Vec<String>,
471    /// Try-catch handler stack
472    try_handlers: Vec<TryHandler>,
473    /// Yielded value (Some when Op::Yield suspends a generator)
474    yielded_value: Option<VmValue>,
475    /// IP at the point of yield (instruction after the Yield op)
476    yielded_ip: usize,
477    /// Current file path (for relative imports)
478    pub file_path: Option<String>,
479    /// Module cache: resolved path → exports
480    module_cache: HashMap<String, HashMap<String, VmValue>>,
481    /// Files currently being imported (circular detection)
482    importing_files: std::collections::HashSet<String>,
483    /// Tracks which globals are public (for module export filtering)
484    pub public_items: std::collections::HashSet<String>,
485    /// Package roots: package_name → source directory
486    pub package_roots: HashMap<String, std::path::PathBuf>,
487    /// Project root (where tl.toml lives)
488    pub project_root: Option<std::path::PathBuf>,
489    /// Schema registry for versioned schemas
490    pub schema_registry: crate::schema::SchemaRegistry,
491    /// Secret vault for credential management (zeroed on drop)
492    pub secret_vault: SecretVault,
493    /// Security policy (optional, set via --sandbox)
494    pub security_policy: Option<crate::security::SecurityPolicy>,
495    /// Tokio runtime for async builtins (lazily initialized)
496    #[cfg(feature = "async-runtime")]
497    runtime: Option<Arc<tokio::runtime::Runtime>>,
498    /// Stashed thrown value for structured error preservation in try/catch
499    thrown_value: Option<VmValue>,
500    /// GPU operations dispatcher (lazily initialized)
501    #[cfg(feature = "gpu")]
502    gpu_ops: Option<tl_gpu::GpuOps>,
503    /// MCP clients associated with agents (agent_name -> clients)
504    #[cfg(feature = "mcp")]
505    mcp_agent_clients: HashMap<String, Vec<Arc<tl_mcp::McpClient>>>,
506}
507
508/// A secret vault that zeros entries on drop to reduce credential exposure in memory.
509#[derive(Debug, Clone, Default)]
510pub struct SecretVault(HashMap<String, String>);
511
512impl SecretVault {
513    pub fn new() -> Self {
514        Self(HashMap::new())
515    }
516    pub fn get(&self, key: &str) -> Option<&String> {
517        self.0.get(key)
518    }
519    pub fn insert(&mut self, key: String, val: String) {
520        self.0.insert(key, val);
521    }
522    pub fn remove(&mut self, key: &str) {
523        self.0.remove(key);
524    }
525    pub fn keys(&self) -> impl Iterator<Item = &String> {
526        self.0.keys()
527    }
528}
529
530impl Drop for SecretVault {
531    fn drop(&mut self) {
532        for val in self.0.values_mut() {
533            // Overwrite the string's buffer with zeros before deallocation.
534            // SAFETY: we write zeros into the valid allocated range of the String.
535            unsafe {
536                let ptr = val.as_mut_vec().as_mut_ptr();
537                std::ptr::write_bytes(ptr, 0, val.len());
538            }
539        }
540        self.0.clear();
541    }
542}
543
544impl Vm {
545    pub fn new() -> Self {
546        let mut vm = Vm {
547            stack: Vec::with_capacity(256),
548            frames: Vec::new(),
549            globals: HashMap::new(),
550            #[cfg(feature = "native")]
551            data_engine: None,
552            output: Vec::new(),
553            try_handlers: Vec::new(),
554            yielded_value: None,
555            yielded_ip: 0,
556            file_path: None,
557            module_cache: HashMap::new(),
558            importing_files: std::collections::HashSet::new(),
559            public_items: std::collections::HashSet::new(),
560            package_roots: HashMap::new(),
561            project_root: None,
562            schema_registry: crate::schema::SchemaRegistry::new(),
563            secret_vault: SecretVault::new(),
564            security_policy: None,
565            #[cfg(feature = "async-runtime")]
566            runtime: None,
567            thrown_value: None,
568            #[cfg(feature = "gpu")]
569            gpu_ops: None,
570            #[cfg(feature = "mcp")]
571            mcp_agent_clients: HashMap::new(),
572        };
573        // Phase 27: Register built-in error enum definitions
574        vm.globals.insert(
575            "DataError".into(),
576            VmValue::EnumDef(Arc::new(VmEnumDef {
577                name: Arc::from("DataError"),
578                variants: vec![
579                    (Arc::from("ParseError"), 2),
580                    (Arc::from("SchemaError"), 3),
581                    (Arc::from("ValidationError"), 2),
582                    (Arc::from("NotFound"), 1),
583                ],
584            })),
585        );
586        vm.globals.insert(
587            "NetworkError".into(),
588            VmValue::EnumDef(Arc::new(VmEnumDef {
589                name: Arc::from("NetworkError"),
590                variants: vec![
591                    (Arc::from("ConnectionError"), 2),
592                    (Arc::from("TimeoutError"), 1),
593                    (Arc::from("HttpError"), 2),
594                ],
595            })),
596        );
597        vm.globals.insert(
598            "ConnectorError".into(),
599            VmValue::EnumDef(Arc::new(VmEnumDef {
600                name: Arc::from("ConnectorError"),
601                variants: vec![
602                    (Arc::from("AuthError"), 2),
603                    (Arc::from("QueryError"), 2),
604                    (Arc::from("ConfigError"), 2),
605                ],
606            })),
607        );
608        // Phase 3: Register MCP builtins as globals
609        #[cfg(feature = "mcp")]
610        {
611            vm.globals.insert(
612                "mcp_connect".to_string(),
613                VmValue::Builtin(BuiltinId::McpConnect),
614            );
615            vm.globals.insert(
616                "mcp_list_tools".to_string(),
617                VmValue::Builtin(BuiltinId::McpListTools),
618            );
619            vm.globals.insert(
620                "mcp_call_tool".to_string(),
621                VmValue::Builtin(BuiltinId::McpCallTool),
622            );
623            vm.globals.insert(
624                "mcp_disconnect".to_string(),
625                VmValue::Builtin(BuiltinId::McpDisconnect),
626            );
627            vm.globals.insert(
628                "mcp_serve".to_string(),
629                VmValue::Builtin(BuiltinId::McpServe),
630            );
631            vm.globals.insert(
632                "mcp_server_info".to_string(),
633                VmValue::Builtin(BuiltinId::McpServerInfo),
634            );
635            vm.globals
636                .insert("mcp_ping".to_string(), VmValue::Builtin(BuiltinId::McpPing));
637            vm.globals.insert(
638                "mcp_list_resources".to_string(),
639                VmValue::Builtin(BuiltinId::McpListResources),
640            );
641            vm.globals.insert(
642                "mcp_read_resource".to_string(),
643                VmValue::Builtin(BuiltinId::McpReadResource),
644            );
645            vm.globals.insert(
646                "mcp_list_prompts".to_string(),
647                VmValue::Builtin(BuiltinId::McpListPrompts),
648            );
649            vm.globals.insert(
650                "mcp_get_prompt".to_string(),
651                VmValue::Builtin(BuiltinId::McpGetPrompt),
652            );
653        }
654        vm
655    }
656
657    /// Lazily initialize and return the tokio runtime.
658    #[cfg(feature = "async-runtime")]
659    fn ensure_runtime(&mut self) -> Arc<tokio::runtime::Runtime> {
660        if self.runtime.is_none() {
661            self.runtime = Some(Arc::new(
662                tokio::runtime::Builder::new_multi_thread()
663                    .enable_all()
664                    .build()
665                    .expect("Failed to create tokio runtime"),
666            ));
667        }
668        self.runtime.as_ref().unwrap().clone()
669    }
670
671    /// Lazily initialize and return the GPU ops dispatcher.
672    #[cfg(feature = "gpu")]
673    fn get_gpu_ops(&mut self) -> Result<&tl_gpu::GpuOps, TlError> {
674        if self.gpu_ops.is_none() {
675            let device =
676                tl_gpu::GpuDevice::get().ok_or_else(|| runtime_err("No GPU device available"))?;
677            self.gpu_ops = Some(tl_gpu::GpuOps::new(device));
678        }
679        Ok(self.gpu_ops.as_ref().unwrap())
680    }
681
682    /// Extract a GpuTensor from a VmValue, auto-uploading CPU tensors if needed.
683    #[cfg(feature = "gpu")]
684    fn ensure_gpu_tensor(&mut self, val: &VmValue) -> Result<Arc<tl_gpu::GpuTensor>, TlError> {
685        match val {
686            VmValue::GpuTensor(gt) => Ok(gt.clone()),
687            #[cfg(feature = "native")]
688            VmValue::Tensor(t) => {
689                let device = tl_gpu::GpuDevice::get()
690                    .ok_or_else(|| runtime_err("No GPU device available"))?;
691                Ok(Arc::new(tl_gpu::GpuTensor::from_cpu(t, device)))
692            }
693            _ => Err(runtime_err(format!(
694                "Expected tensor or gpu_tensor, got {}",
695                val.type_name()
696            ))),
697        }
698    }
699
700    #[cfg(feature = "native")]
701    fn engine(&mut self) -> &DataEngine {
702        if self.data_engine.is_none() {
703            self.data_engine = Some(DataEngine::new());
704        }
705        self.data_engine.as_ref().unwrap()
706    }
707
708    /// Ensure the stack has at least `size` slots.
709    fn ensure_stack(&mut self, size: usize) {
710        if self.stack.len() < size {
711            self.stack.resize(size, VmValue::None);
712        }
713    }
714
715    /// Execute a compiled prototype.
716    pub fn execute(&mut self, proto: &Prototype) -> Result<VmValue, TlError> {
717        let proto = Arc::new(proto.clone());
718        let base = self.stack.len();
719        self.ensure_stack(base + proto.num_registers as usize + 1);
720
721        self.frames.push(CallFrame {
722            prototype: proto,
723            ip: 0,
724            base,
725            upvalues: Vec::new(),
726        });
727
728        self.run().map_err(|e| self.enrich_error(e))
729    }
730
731    // -- Debug API (Phase H5) --
732
733    /// Prepare the VM for debug execution by pushing a call frame without running.
734    pub fn debug_load(&mut self, proto: &Prototype) {
735        let proto = Arc::new(proto.clone());
736        let base = self.stack.len();
737        self.ensure_stack(base + proto.num_registers as usize + 1);
738        self.frames.push(CallFrame {
739            prototype: proto,
740            ip: 0,
741            base,
742            upvalues: Vec::new(),
743        });
744    }
745
746    /// Execute a single instruction in debug mode. Returns:
747    /// - Ok(None) → instruction executed, more to go
748    /// - Ok(Some(val)) → execution completed with return value
749    /// - Err → runtime error
750    pub fn debug_step(&mut self) -> Result<Option<VmValue>, TlError> {
751        let entry_depth = 1; // Always run at top level depth
752        self.run_step(entry_depth).map_err(|e| self.enrich_error(e))
753    }
754
755    /// Get the current source line number (1-based) or 0 if unknown.
756    pub fn debug_current_line(&self) -> u32 {
757        if let Some(frame) = self.frames.last() {
758            let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
759            if ip < frame.prototype.lines.len() {
760                frame.prototype.lines[ip]
761            } else {
762                0
763            }
764        } else {
765            0
766        }
767    }
768
769    /// Get the current function name being executed.
770    pub fn debug_current_function(&self) -> String {
771        self.frames
772            .last()
773            .map(|f| f.prototype.name.clone())
774            .unwrap_or_default()
775    }
776
777    /// Check if the VM has finished executing (no more frames).
778    pub fn debug_is_done(&self) -> bool {
779        self.frames.is_empty()
780            || self
781                .frames
782                .last()
783                .is_some_and(|f| f.ip >= f.prototype.code.len())
784    }
785
786    /// Get a global variable by name.
787    pub fn debug_get_global(&self, name: &str) -> Option<&VmValue> {
788        self.globals.get(name)
789    }
790
791    /// Get a local variable by name (looks in top_level_locals of current frame).
792    pub fn debug_get_local(&self, name: &str) -> Option<&VmValue> {
793        if let Some(frame) = self.frames.last() {
794            for (local_name, reg) in &frame.prototype.top_level_locals {
795                if local_name == name {
796                    let idx = frame.base + *reg as usize;
797                    if idx < self.stack.len() {
798                        return Some(&self.stack[idx]);
799                    }
800                }
801            }
802        }
803        None
804    }
805
806    /// Get all local variables in the current frame.
807    pub fn debug_locals(&self) -> Vec<(String, &VmValue)> {
808        let mut result = Vec::new();
809        if let Some(frame) = self.frames.last() {
810            for (name, reg) in &frame.prototype.top_level_locals {
811                let idx = frame.base + *reg as usize;
812                if idx < self.stack.len() {
813                    result.push((name.clone(), &self.stack[idx]));
814                }
815            }
816        }
817        result
818    }
819
820    /// Get the current IP (instruction pointer).
821    pub fn debug_current_ip(&self) -> usize {
822        self.frames.last().map(|f| f.ip).unwrap_or(0)
823    }
824
825    /// Run until the next source line changes (step over).
826    pub fn debug_step_line(&mut self) -> Result<Option<VmValue>, TlError> {
827        let start_line = self.debug_current_line();
828        loop {
829            if self.debug_is_done() {
830                return Ok(Some(VmValue::None));
831            }
832            let result = self.debug_step()?;
833            if result.is_some() {
834                return Ok(result);
835            }
836            let new_line = self.debug_current_line();
837            if new_line != start_line && new_line != 0 {
838                return Ok(None);
839            }
840        }
841    }
842
843    /// Continue execution until a breakpoint line is hit or execution completes.
844    pub fn debug_continue(&mut self, breakpoints: &[u32]) -> Result<Option<VmValue>, TlError> {
845        loop {
846            if self.debug_is_done() {
847                return Ok(Some(VmValue::None));
848            }
849            let result = self.debug_step()?;
850            if result.is_some() {
851                return Ok(result);
852            }
853            let line = self.debug_current_line();
854            if breakpoints.contains(&line) {
855                return Ok(None);
856            }
857        }
858    }
859
860    /// Enrich a runtime error with line number and stack trace from the current call frames.
861    fn enrich_error(&self, err: TlError) -> TlError {
862        match err {
863            TlError::Runtime(mut re) => {
864                // Build stack trace from remaining frames
865                let mut trace = Vec::new();
866                for frame in self.frames.iter().rev() {
867                    let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
868                    let line = if ip < frame.prototype.lines.len() {
869                        frame.prototype.lines[ip]
870                    } else {
871                        0
872                    };
873                    trace.push(tl_errors::StackFrame {
874                        function: frame.prototype.name.clone(),
875                        line,
876                    });
877                }
878                // Set span from the innermost frame's line if not already set
879                if re.span.is_none() && !trace.is_empty() && trace[0].line > 0 {
880                    // We only have line number, not byte offset, so we can't set a precise span.
881                    // But we can set a line-based marker that report_runtime_error can use.
882                    // For now, leave span as None and rely on the stack trace.
883                }
884                re.stack_trace = trace;
885                TlError::Runtime(re)
886            }
887            other => other,
888        }
889    }
890
891    /// Main dispatch loop. Runs the current (topmost) frame until Return.
892    fn run(&mut self) -> Result<VmValue, TlError> {
893        let entry_depth = self.frames.len();
894        loop {
895            let step_result = self.run_step(entry_depth);
896            match step_result {
897                Ok(Some(val)) => return Ok(val), // Return instruction
898                Ok(None) => continue,            // Normal instruction
899                Err(e) => {
900                    // Check for try handler
901                    if let Some(handler) = self.try_handlers.pop() {
902                        // Restore to handler's frame
903                        while self.frames.len() > handler.frame_idx {
904                            self.frames.pop();
905                        }
906                        if self.frames.is_empty() {
907                            return Err(e);
908                        }
909                        let fidx = self.frames.len() - 1;
910                        self.frames[fidx].ip = handler.catch_ip;
911                        let err_msg = match &e {
912                            TlError::Runtime(re) => re.message.clone(),
913                            other => format!("{other}"),
914                        };
915                        // Put error value in catch scope's first local
916                        // The compiler emits LoadNone for the catch var at catch_ip; we need to
917                        // identify the register, set the error value, and skip past the LoadNone
918                        let catch_val = self
919                            .thrown_value
920                            .take()
921                            .unwrap_or_else(|| VmValue::String(Arc::from(err_msg.as_str())));
922                        let cbase = self.frames[fidx].base;
923                        let current_ip = self.frames[fidx].ip;
924                        if current_ip < self.frames[fidx].prototype.code.len() {
925                            let catch_inst = self.frames[fidx].prototype.code[current_ip];
926                            let catch_op = decode_op(catch_inst);
927                            let catch_reg = decode_a(catch_inst);
928                            if matches!(catch_op, Op::LoadNone) {
929                                // Skip the LoadNone and write error value directly
930                                self.frames[fidx].ip += 1;
931                                self.ensure_stack(cbase + catch_reg as usize + 1);
932                                self.stack[cbase + catch_reg as usize] = catch_val;
933                            }
934                        }
935                        continue;
936                    }
937                    return Err(e);
938                }
939            }
940        }
941    }
942
943    /// Execute a single instruction. Returns Ok(Some(val)) for Return, Ok(None) for continue, Err for errors.
944    fn run_step(&mut self, entry_depth: usize) -> Result<Option<VmValue>, TlError> {
945        if self.frames.len() < entry_depth || self.frames.is_empty() {
946            return Ok(Some(VmValue::None));
947        }
948        let frame_idx = self.frames.len() - 1;
949        let frame = &self.frames[frame_idx];
950
951        if frame.ip >= frame.prototype.code.len() {
952            // End of bytecode — return None
953            self.frames.pop();
954            return Ok(Some(VmValue::None));
955        }
956
957        let inst = frame.prototype.code[frame.ip];
958        let op = decode_op(inst);
959        let a = decode_a(inst);
960        let b = decode_b(inst);
961        let c = decode_c(inst);
962        let bx = decode_bx(inst);
963        let sbx = decode_sbx(inst);
964        let base = frame.base;
965
966        // Advance IP before executing (some ops modify it)
967        self.frames[frame_idx].ip += 1;
968
969        match op {
970            Op::LoadConst => {
971                let val = self.load_constant(frame_idx, bx)?;
972                self.stack[base + a as usize] = val;
973            }
974            Op::LoadNone => {
975                self.stack[base + a as usize] = VmValue::None;
976            }
977            Op::LoadTrue => {
978                self.stack[base + a as usize] = VmValue::Bool(true);
979            }
980            Op::LoadFalse => {
981                self.stack[base + a as usize] = VmValue::Bool(false);
982            }
983            Op::Move => {
984                let val = &self.stack[base + b as usize];
985                if matches!(val, VmValue::Moved) {
986                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
987                }
988                self.stack[base + a as usize] = val.clone();
989            }
990            Op::GetLocal => {
991                let val = &self.stack[base + b as usize];
992                if matches!(val, VmValue::Moved) {
993                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
994                }
995                self.stack[base + a as usize] = val.clone();
996            }
997            Op::SetLocal => {
998                let val = self.stack[base + a as usize].clone();
999                self.stack[base + b as usize] = val;
1000            }
1001            Op::GetGlobal => {
1002                let name = self.get_string_constant(frame_idx, bx)?;
1003                let val = self
1004                    .globals
1005                    .get(name.as_ref())
1006                    .cloned()
1007                    .unwrap_or(VmValue::None);
1008                if matches!(val, VmValue::Moved) {
1009                    return Err(runtime_err(format!(
1010                        "Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."
1011                    )));
1012                }
1013                self.stack[base + a as usize] = val;
1014            }
1015            Op::SetGlobal => {
1016                let name = self.get_string_constant(frame_idx, bx)?;
1017                let val = self.stack[base + a as usize].clone();
1018                // Phase 21: Detect __schema__ and __migrate__ globals and register in schema_registry
1019                #[cfg(feature = "native")]
1020                if let VmValue::String(ref s) = val {
1021                    if s.starts_with("__schema__:") {
1022                        self.process_schema_global(s);
1023                    } else if s.starts_with("__migrate__:") {
1024                        self.process_migrate_global(s);
1025                    }
1026                }
1027                self.globals.insert(name.to_string(), val);
1028            }
1029            Op::GetUpvalue => {
1030                let val = {
1031                    let frame = &self.frames[frame_idx];
1032                    match &frame.upvalues[b as usize] {
1033                        UpvalueRef::Open { stack_index } => self.stack[*stack_index].clone(),
1034                        UpvalueRef::Closed(v) => v.clone(),
1035                    }
1036                };
1037                self.stack[base + a as usize] = val;
1038            }
1039            Op::SetUpvalue => {
1040                let val = self.stack[base + a as usize].clone();
1041                let frame = &mut self.frames[frame_idx];
1042                match &mut frame.upvalues[b as usize] {
1043                    UpvalueRef::Open { stack_index } => {
1044                        let idx = *stack_index;
1045                        self.stack[idx] = val;
1046                    }
1047                    UpvalueRef::Closed(v) => {
1048                        *v = val;
1049                    }
1050                }
1051            }
1052            Op::Add => {
1053                let result = self.vm_add(base, b, c)?;
1054                self.stack[base + a as usize] = result;
1055            }
1056            Op::Sub => {
1057                let result = self.vm_sub(base, b, c)?;
1058                self.stack[base + a as usize] = result;
1059            }
1060            Op::Mul => {
1061                let result = self.vm_mul(base, b, c)?;
1062                self.stack[base + a as usize] = result;
1063            }
1064            Op::Div => {
1065                let result = self.vm_div(base, b, c)?;
1066                self.stack[base + a as usize] = result;
1067            }
1068            Op::Mod => {
1069                let result = self.vm_mod(base, b, c)?;
1070                self.stack[base + a as usize] = result;
1071            }
1072            Op::Pow => {
1073                let result = self.vm_pow(base, b, c)?;
1074                self.stack[base + a as usize] = result;
1075            }
1076            Op::Neg => {
1077                let result = match &self.stack[base + b as usize] {
1078                    VmValue::Int(n) => VmValue::Int(-n),
1079                    VmValue::Float(n) => VmValue::Float(-n),
1080                    VmValue::Decimal(d) => VmValue::Decimal(-d),
1081                    other => {
1082                        return Err(runtime_err(format!("Cannot negate {}", other.type_name())));
1083                    }
1084                };
1085                self.stack[base + a as usize] = result;
1086            }
1087            Op::Eq => {
1088                let result = self.vm_eq(base, b, c);
1089                self.stack[base + a as usize] = VmValue::Bool(result);
1090            }
1091            Op::Neq => {
1092                let result = !self.vm_eq(base, b, c);
1093                self.stack[base + a as usize] = VmValue::Bool(result);
1094            }
1095            Op::Lt => {
1096                let result = self.vm_cmp(base, b, c)?;
1097                self.stack[base + a as usize] = VmValue::Bool(result == Some(-1));
1098            }
1099            Op::Gt => {
1100                let result = self.vm_cmp(base, b, c)?;
1101                self.stack[base + a as usize] = VmValue::Bool(result == Some(1));
1102            }
1103            Op::Lte => {
1104                let result = self.vm_cmp(base, b, c)?;
1105                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(-1) | Some(0)));
1106            }
1107            Op::Gte => {
1108                let result = self.vm_cmp(base, b, c)?;
1109                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(0) | Some(1)));
1110            }
1111            Op::And => {
1112                let left = self.stack[base + b as usize].is_truthy();
1113                let right = self.stack[base + c as usize].is_truthy();
1114                self.stack[base + a as usize] = VmValue::Bool(left && right);
1115            }
1116            Op::Or => {
1117                let left = self.stack[base + b as usize].is_truthy();
1118                let right = self.stack[base + c as usize].is_truthy();
1119                self.stack[base + a as usize] = VmValue::Bool(left || right);
1120            }
1121            Op::Not => {
1122                let val = !self.stack[base + b as usize].is_truthy();
1123                self.stack[base + a as usize] = VmValue::Bool(val);
1124            }
1125            Op::Concat => {
1126                let left = format!("{}", self.stack[base + b as usize]);
1127                let right = format!("{}", self.stack[base + c as usize]);
1128                self.stack[base + a as usize] =
1129                    VmValue::String(Arc::from(format!("{left}{right}").as_str()));
1130            }
1131            Op::Jump => {
1132                let frame = &mut self.frames[frame_idx];
1133                frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1134            }
1135            Op::JumpIfFalse => {
1136                if !self.stack[base + a as usize].is_truthy() {
1137                    let frame = &mut self.frames[frame_idx];
1138                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1139                }
1140            }
1141            Op::JumpIfTrue => {
1142                if self.stack[base + a as usize].is_truthy() {
1143                    let frame = &mut self.frames[frame_idx];
1144                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1145                }
1146            }
1147            Op::Call => {
1148                // a = func reg, b = args start, c = arg count
1149                let func_val = self.stack[base + a as usize].clone();
1150                self.do_call(func_val, base, a, b, c)?;
1151            }
1152            Op::Return => {
1153                let return_val = self.stack[base + a as usize].clone();
1154                self.frames.pop();
1155                return Ok(Some(return_val));
1156            }
1157            Op::Closure => {
1158                let proto = match &self.frames[frame_idx].prototype.constants[bx as usize] {
1159                    Constant::Prototype(p) => p.clone(),
1160                    _ => return Err(runtime_err("Expected prototype constant")),
1161                };
1162
1163                // Capture upvalues
1164                let mut upvalues = Vec::new();
1165                for def in &proto.upvalue_defs {
1166                    if def.is_local {
1167                        upvalues.push(UpvalueRef::Open {
1168                            stack_index: base + def.index as usize,
1169                        });
1170                    } else {
1171                        let frame = &self.frames[frame_idx];
1172                        upvalues.push(frame.upvalues[def.index as usize].clone());
1173                    }
1174                }
1175
1176                let closure = VmClosure {
1177                    prototype: proto,
1178                    upvalues,
1179                };
1180                self.stack[base + a as usize] = VmValue::Function(Arc::new(closure));
1181            }
1182            Op::NewList => {
1183                // a = dest, b = start reg, c = count
1184                let mut items = Vec::with_capacity(c as usize);
1185                for i in 0..c as usize {
1186                    items.push(self.stack[base + b as usize + i].clone());
1187                }
1188                self.stack[base + a as usize] = VmValue::List(Box::new(items));
1189            }
1190            Op::GetIndex => {
1191                let raw_obj = &self.stack[base + b as usize];
1192                let obj = match raw_obj {
1193                    VmValue::Ref(inner) => inner.as_ref(),
1194                    other => other,
1195                };
1196                let idx = &self.stack[base + c as usize];
1197                let result = match (obj, idx) {
1198                    (VmValue::List(items), VmValue::Int(i)) => {
1199                        let idx = if *i < 0 {
1200                            let adjusted = items.len() as i64 + *i;
1201                            if adjusted < 0 {
1202                                return Err(runtime_err(format!(
1203                                    "Index {} out of bounds for list of length {}",
1204                                    i,
1205                                    items.len()
1206                                )));
1207                            }
1208                            adjusted as usize
1209                        } else {
1210                            *i as usize
1211                        };
1212                        items.get(idx).cloned().ok_or_else(|| {
1213                            runtime_err(format!(
1214                                "Index {} out of bounds for list of length {}",
1215                                i,
1216                                items.len()
1217                            ))
1218                        })?
1219                    }
1220                    (VmValue::Map(pairs), VmValue::String(key)) => pairs
1221                        .iter()
1222                        .find(|(k, _)| k.as_ref() == key.as_ref())
1223                        .map(|(_, v)| v.clone())
1224                        .unwrap_or(VmValue::None),
1225                    _ => {
1226                        return Err(runtime_err(format!(
1227                            "Cannot index {} with {}",
1228                            obj.type_name(),
1229                            idx.type_name()
1230                        )));
1231                    }
1232                };
1233                self.stack[base + a as usize] = result;
1234            }
1235            Op::SetIndex => {
1236                if matches!(&self.stack[base + b as usize], VmValue::Ref(_)) {
1237                    return Err(runtime_err(
1238                        "Cannot mutate a borrowed reference".to_string(),
1239                    ));
1240                }
1241                let val = self.stack[base + a as usize].clone();
1242                let idx_val = self.stack[base + c as usize].clone();
1243                match idx_val {
1244                    VmValue::Int(i) => {
1245                        if let VmValue::List(ref mut items) = self.stack[base + b as usize] {
1246                            let idx = if i < 0 {
1247                                let adjusted = items.len() as i64 + i;
1248                                if adjusted < 0 {
1249                                    return Err(runtime_err(format!(
1250                                        "Index {} out of bounds for list of length {}",
1251                                        i,
1252                                        items.len()
1253                                    )));
1254                                }
1255                                adjusted as usize
1256                            } else {
1257                                i as usize
1258                            };
1259                            if idx < items.len() {
1260                                items[idx] = val;
1261                            } else {
1262                                return Err(runtime_err(format!(
1263                                    "Index {} out of bounds for list of length {}",
1264                                    i,
1265                                    items.len()
1266                                )));
1267                            }
1268                        }
1269                    }
1270                    VmValue::String(key) => {
1271                        if let VmValue::Map(ref mut pairs) = self.stack[base + b as usize] {
1272                            if let Some(entry) =
1273                                pairs.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref())
1274                            {
1275                                entry.1 = val;
1276                            } else {
1277                                pairs.push((key, val));
1278                            }
1279                        }
1280                    }
1281                    _ => {}
1282                }
1283            }
1284            Op::NewMap => {
1285                // a = dest, b = start reg, c = pair count
1286                // The pairs are key, value, key, value in registers b..b+c*2
1287                let mut pairs = Vec::with_capacity(c as usize);
1288                for i in 0..c as usize {
1289                    let key_val = &self.stack[base + b as usize + i * 2];
1290                    let val = self.stack[base + b as usize + i * 2 + 1].clone();
1291                    let key = match key_val {
1292                        VmValue::String(s) => s.clone(),
1293                        other => Arc::from(format!("{other}").as_str()),
1294                    };
1295                    pairs.push((key, val));
1296                }
1297                self.stack[base + a as usize] = VmValue::Map(Box::new(pairs));
1298            }
1299            Op::TablePipe => {
1300                #[cfg(feature = "native")]
1301                {
1302                    // a = table reg, b = op name constant idx, c = args constant idx
1303                    let table_val = self.stack[base + a as usize].clone();
1304                    let result = self.handle_table_pipe(frame_idx, table_val, b, c)?;
1305                    self.stack[base + a as usize] = result;
1306                }
1307                #[cfg(not(feature = "native"))]
1308                {
1309                    let _ = (a, b, c, frame_idx);
1310                    return Err(runtime_err("Table operations not available in WASM"));
1311                }
1312            }
1313            Op::CallBuiltin => {
1314                // ABx format: a = dest, bx = builtin id (16-bit)
1315                // Next instruction: A = arg count, B = first arg reg
1316                let builtin_id = decode_bx(inst);
1317                let next_inst = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1318                self.frames[frame_idx].ip += 1;
1319                let arg_count = decode_a(next_inst) as usize;
1320                let first_arg = decode_b(next_inst) as usize;
1321
1322                let result = self.call_builtin(builtin_id, base + first_arg, arg_count)?;
1323                self.stack[base + a as usize] = result;
1324            }
1325            Op::ForIter => {
1326                // a = iterator (index) reg, b = list reg, c = value dest reg
1327                let idx = match &self.stack[base + a as usize] {
1328                    VmValue::Int(i) => *i as usize,
1329                    _ => 0,
1330                };
1331                let list = &self.stack[base + b as usize];
1332                let done = match list {
1333                    VmValue::List(items) if idx < items.len() => {
1334                        let item = items[idx].clone();
1335                        self.stack[base + c as usize] = item;
1336                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1337                        false
1338                    }
1339                    VmValue::Map(pairs) if idx < pairs.len() => {
1340                        let (k, v) = &pairs[idx];
1341                        let pair =
1342                            VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]));
1343                        self.stack[base + c as usize] = pair;
1344                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1345                        false
1346                    }
1347                    VmValue::Set(items) if idx < items.len() => {
1348                        let item = items[idx].clone();
1349                        self.stack[base + c as usize] = item;
1350                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1351                        false
1352                    }
1353                    VmValue::Generator(gen_arc) => {
1354                        let g = gen_arc.clone();
1355                        let val = self.generator_next(&g)?;
1356                        if matches!(val, VmValue::None) {
1357                            true
1358                        } else {
1359                            self.stack[base + c as usize] = val;
1360                            false
1361                        }
1362                    }
1363                    _ => true,
1364                };
1365                if done {
1366                    // Next instruction is a Jump — execute it
1367                    // (the jump instruction follows ForIter)
1368                } else {
1369                    // Skip the jump instruction
1370                    self.frames[frame_idx].ip += 1;
1371                }
1372            }
1373            Op::ForPrep => {
1374                // Not currently used — ForIter handles everything
1375            }
1376            Op::TestMatch => {
1377                // a = subject reg, b = pattern reg, c = dest bool reg
1378                let subject = &self.stack[base + a as usize];
1379                let pattern = &self.stack[base + b as usize];
1380                let matched = match (subject, pattern) {
1381                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
1382                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
1383                    (VmValue::String(a), VmValue::String(b)) => a == b,
1384                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1385                    (VmValue::None, VmValue::None) => true,
1386                    // Enum instance matching: same type + same variant
1387                    (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1388                        subj.type_name == pat.type_name && subj.variant == pat.variant
1389                    }
1390                    // Struct instance matching by type name
1391                    (VmValue::StructInstance(s), VmValue::String(name)) => {
1392                        s.type_name.as_ref() == name.as_ref()
1393                    }
1394                    _ => false,
1395                };
1396                self.stack[base + c as usize] = VmValue::Bool(matched);
1397            }
1398            Op::NullCoalesce => {
1399                if matches!(self.stack[base + a as usize], VmValue::None) {
1400                    let val = self.stack[base + b as usize].clone();
1401                    self.stack[base + a as usize] = val;
1402                }
1403            }
1404            Op::GetMember => {
1405                // a = dest, b = object reg, c = field name constant
1406                let field_name = self.get_string_constant(frame_idx, c as u16)?;
1407                let raw_obj = self.stack[base + b as usize].clone();
1408                let obj = match &raw_obj {
1409                    VmValue::Ref(inner) => inner.as_ref().clone(),
1410                    _ => raw_obj,
1411                };
1412                let result = match &obj {
1413                    VmValue::StructInstance(inst) => inst
1414                        .fields
1415                        .iter()
1416                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1417                        .map(|(_, v)| v.clone())
1418                        .unwrap_or(VmValue::None),
1419                    VmValue::Module(m) => m
1420                        .exports
1421                        .get(field_name.as_ref())
1422                        .cloned()
1423                        .unwrap_or(VmValue::None),
1424                    VmValue::EnumInstance(e) => match field_name.as_ref() {
1425                        "variant" => VmValue::String(e.variant.clone()),
1426                        "type_name" => VmValue::String(e.type_name.clone()),
1427                        _ => VmValue::None,
1428                    },
1429                    VmValue::Map(pairs) => pairs
1430                        .iter()
1431                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1432                        .map(|(_, v)| v.clone())
1433                        .unwrap_or(VmValue::None),
1434                    #[cfg(feature = "python")]
1435                    VmValue::PyObject(wrapper) => {
1436                        crate::python::py_get_member(wrapper, field_name.as_ref())
1437                    }
1438                    _ => VmValue::None,
1439                };
1440                self.stack[base + a as usize] = result;
1441            }
1442            Op::Interpolate => {
1443                // a = dest, bx = string template constant
1444                let template = self.get_string_constant(frame_idx, bx)?;
1445                let result = self.interpolate_string(&template, base)?;
1446                self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1447            }
1448            Op::Train => {
1449                #[cfg(feature = "native")]
1450                {
1451                    let result = self.handle_train(frame_idx, b, c)?;
1452                    self.stack[base + a as usize] = result;
1453                }
1454                #[cfg(not(feature = "native"))]
1455                {
1456                    let _ = (a, b, c, frame_idx);
1457                    return Err(runtime_err("AI training not available in WASM"));
1458                }
1459            }
1460            Op::PipelineExec => {
1461                #[cfg(feature = "native")]
1462                {
1463                    let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1464                    self.stack[base + a as usize] = result;
1465                }
1466                #[cfg(not(feature = "native"))]
1467                {
1468                    let _ = (a, b, c, frame_idx);
1469                    return Err(runtime_err("Pipelines not available in WASM"));
1470                }
1471            }
1472            Op::StreamExec => {
1473                #[cfg(feature = "native")]
1474                {
1475                    let result = self.handle_stream_exec(frame_idx, b)?;
1476                    self.stack[base + a as usize] = result;
1477                }
1478                #[cfg(not(feature = "native"))]
1479                {
1480                    let _ = (a, b, frame_idx);
1481                    return Err(runtime_err("Streaming not available in WASM"));
1482                }
1483            }
1484            Op::ConnectorDecl => {
1485                #[cfg(feature = "native")]
1486                {
1487                    let result = self.handle_connector_decl(frame_idx, b, c)?;
1488                    self.stack[base + a as usize] = result;
1489                }
1490                #[cfg(not(feature = "native"))]
1491                {
1492                    let _ = (a, b, c, frame_idx);
1493                    return Err(runtime_err("Connectors not available in WASM"));
1494                }
1495            }
1496
1497            // ── Phase 5: Language completeness opcodes ──
1498            Op::NewStruct => {
1499                // Two uses:
1500                // 1) Struct declaration: a=dest, b=name_const, c=fields_const (AstExprList)
1501                //    Next instruction is NOT a Move with start reg
1502                // 2) Struct instance: a=dest, b=name_const, c=field_count
1503                //    Next instruction is Move with start reg in A
1504
1505                let name = self.get_string_constant(frame_idx, b as u16)?;
1506
1507                // High bit of c distinguishes declaration (set) from instance (clear).
1508                // Declarations: c = constant_idx | 0x80
1509                // Instances: c = field_count (no high bit)
1510                let is_decl = (c & 0x80) != 0;
1511
1512                if is_decl {
1513                    let const_idx = (c & 0x7F) as usize;
1514                    // Struct/Enum declaration
1515                    let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1516                        Constant::AstExprList(exprs) => exprs.clone(),
1517                        _ => Vec::new(),
1518                    };
1519                    // Check if it looks like an enum (fields have "Name:count" format)
1520                    let is_enum = fields_data
1521                        .first()
1522                        .map(|e| {
1523                            if let AstExpr::String(s) = e {
1524                                s.contains(':')
1525                            } else {
1526                                false
1527                            }
1528                        })
1529                        .unwrap_or(false);
1530
1531                    if is_enum {
1532                        let variants: Vec<(Arc<str>, usize)> = fields_data
1533                            .iter()
1534                            .filter_map(|e| {
1535                                if let AstExpr::String(s) = e {
1536                                    let parts: Vec<&str> = s.splitn(2, ':').collect();
1537                                    if parts.len() == 2 {
1538                                        Some((
1539                                            Arc::from(parts[0]),
1540                                            parts[1].parse::<usize>().unwrap_or(0),
1541                                        ))
1542                                    } else {
1543                                        None
1544                                    }
1545                                } else {
1546                                    None
1547                                }
1548                            })
1549                            .collect();
1550                        self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1551                            name: name.clone(),
1552                            variants,
1553                        }));
1554                    } else {
1555                        let field_names: Vec<Arc<str>> = fields_data
1556                            .iter()
1557                            .filter_map(|e| {
1558                                if let AstExpr::String(s) = e {
1559                                    Some(Arc::from(s.as_str()))
1560                                } else {
1561                                    None
1562                                }
1563                            })
1564                            .collect();
1565                        self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1566                            name: name.clone(),
1567                            fields: field_names,
1568                        }));
1569                    }
1570                } else {
1571                    // Struct instance creation: c = field count
1572                    let field_count = c as usize;
1573                    // Next instruction holds start register in A field
1574                    let next_ip = self.frames[frame_idx].ip;
1575                    let next = self.frames[frame_idx]
1576                        .prototype
1577                        .code
1578                        .get(next_ip)
1579                        .copied()
1580                        .unwrap_or(0);
1581                    let start_reg = decode_a(next) as usize;
1582                    self.frames[frame_idx].ip += 1; // skip the extra instruction
1583
1584                    let mut fields = Vec::new();
1585                    for i in 0..field_count {
1586                        let fname = self.stack[base + start_reg + i * 2].clone();
1587                        let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1588                        let fname_str = match fname {
1589                            VmValue::String(s) => s,
1590                            _ => Arc::from(format!("field_{i}").as_str()),
1591                        };
1592                        fields.push((fname_str, fval));
1593                    }
1594                    self.stack[base + a as usize] =
1595                        VmValue::StructInstance(Arc::new(VmStructInstance {
1596                            type_name: name.clone(),
1597                            fields,
1598                        }));
1599                }
1600            }
1601
1602            Op::SetMember => {
1603                if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1604                    return Err(runtime_err(
1605                        "Cannot mutate a borrowed reference".to_string(),
1606                    ));
1607                }
1608                // a = object reg, b = field name constant, c = value reg
1609                let field_name = self.get_string_constant(frame_idx, b as u16)?;
1610                let val = self.stack[base + c as usize].clone();
1611                let obj = self.stack[base + a as usize].clone();
1612                if let VmValue::StructInstance(inst) = obj {
1613                    let mut new_fields = inst.fields.clone();
1614                    let mut found = false;
1615                    for (k, v) in &mut new_fields {
1616                        if k.as_ref() == field_name.as_ref() {
1617                            *v = val.clone();
1618                            found = true;
1619                            break;
1620                        }
1621                    }
1622                    if !found {
1623                        new_fields.push((field_name, val));
1624                    }
1625                    self.stack[base + a as usize] =
1626                        VmValue::StructInstance(Arc::new(VmStructInstance {
1627                            type_name: inst.type_name.clone(),
1628                            fields: new_fields,
1629                        }));
1630                }
1631            }
1632
1633            Op::NewEnum => {
1634                // a = dest, b = name constant ("EnumName::Variant"), c = args start reg
1635                // Next instruction: arg_count in A field
1636                let full_name = self.get_string_constant(frame_idx, b as u16)?;
1637                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1638                self.frames[frame_idx].ip += 1;
1639                let arg_count = decode_a(next) as usize;
1640                let args_start = c as usize;
1641
1642                // Parse "EnumName::Variant"
1643                let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1644                let (type_name, variant) = if parts.len() == 2 {
1645                    (Arc::from(parts[0]), Arc::from(parts[1]))
1646                } else {
1647                    (Arc::from(""), Arc::from(full_name.as_ref()))
1648                };
1649
1650                let mut fields = Vec::new();
1651                for i in 0..arg_count {
1652                    fields.push(self.stack[base + args_start + i].clone());
1653                }
1654
1655                self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1656                    type_name,
1657                    variant,
1658                    fields,
1659                }));
1660            }
1661
1662            Op::MatchEnum => {
1663                // a = subject reg, b = variant name constant, c = dest bool reg
1664                let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1665                let subject = &self.stack[base + a as usize];
1666                let matched = match subject {
1667                    VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1668                    _ => false,
1669                };
1670                self.stack[base + c as usize] = VmValue::Bool(matched);
1671            }
1672
1673            Op::MethodCall => {
1674                // a = dest, b = object reg, c = method name constant
1675                // Next instruction: A = args_start, B = arg_count
1676                let method_name = self.get_string_constant(frame_idx, c as u16)?;
1677                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1678                self.frames[frame_idx].ip += 1;
1679                let args_start = decode_a(next) as usize;
1680                let arg_count = decode_b(next) as usize;
1681
1682                let obj = self.stack[base + b as usize].clone();
1683                let mut args = Vec::new();
1684                for i in 0..arg_count {
1685                    args.push(self.stack[base + args_start + i].clone());
1686                }
1687
1688                let result = self.dispatch_method(obj, &method_name, &args)?;
1689                self.stack[base + a as usize] = result;
1690            }
1691
1692            Op::Throw => {
1693                // a = value register
1694                let val = self.stack[base + a as usize].clone();
1695                self.thrown_value = Some(val.clone());
1696                let err_msg = format!("{val}");
1697                return Err(runtime_err(err_msg));
1698            }
1699
1700            Op::TryBegin => {
1701                // sbx = offset to catch handler (relative to this instruction)
1702                let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1703                self.try_handlers.push(TryHandler {
1704                    frame_idx: self.frames.len(),
1705                    catch_ip,
1706                });
1707            }
1708
1709            Op::TryEnd => {
1710                // Pop the try handler (success path)
1711                self.try_handlers.pop();
1712            }
1713
1714            Op::Import => {
1715                #[cfg(feature = "native")]
1716                {
1717                    // a = dest, bx = path constant
1718                    // Next instruction encodes either:
1719                    //   - Classic import: A = alias constant, B = 0, C = 0
1720                    //   - Use import: A = extra, B = kind, C = 0xAB (magic marker)
1721                    let path = self.get_string_constant(frame_idx, bx)?;
1722                    let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1723                    self.frames[frame_idx].ip += 1;
1724                    let next_a = decode_a(next);
1725                    let next_b = decode_b(next);
1726                    let next_c = decode_c(next);
1727
1728                    let result = if next_c == 0xAB {
1729                        // Use-style import (dot-path)
1730                        self.handle_use_import(&path, next_a, next_b, frame_idx)?
1731                    } else {
1732                        // Classic import "file.tl" [as alias]
1733                        let alias_idx = next_a as u16;
1734                        let alias = self.get_string_constant(frame_idx, alias_idx)?;
1735                        self.handle_import(&path, &alias)?
1736                    };
1737                    self.stack[base + a as usize] = result;
1738                }
1739                #[cfg(not(feature = "native"))]
1740                {
1741                    let _ = (a, bx, frame_idx);
1742                    return Err(runtime_err("Module imports not available in WASM"));
1743                }
1744            }
1745
1746            Op::Await => {
1747                // a = dest, b = task/value register
1748                let val = self.stack[base + b as usize].clone();
1749                match val {
1750                    VmValue::Task(task) => {
1751                        let rx = {
1752                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1753                            guard.take()
1754                        };
1755                        match rx {
1756                            Some(receiver) => match receiver.recv() {
1757                                Ok(Ok(result)) => {
1758                                    self.stack[base + a as usize] = result;
1759                                }
1760                                Ok(Err(err_msg)) => {
1761                                    return Err(runtime_err(err_msg));
1762                                }
1763                                Err(_) => {
1764                                    return Err(runtime_err("Task channel disconnected"));
1765                                }
1766                            },
1767                            None => {
1768                                return Err(runtime_err("Task already awaited"));
1769                            }
1770                        }
1771                    }
1772                    // Non-task values pass through
1773                    other => {
1774                        self.stack[base + a as usize] = other;
1775                    }
1776                }
1777            }
1778            Op::Yield => {
1779                // a = value register to yield
1780                let val = self.stack[base + a as usize].clone();
1781                self.yielded_value = Some(val.clone());
1782                // Save the current ip (already advanced past Yield instruction)
1783                self.yielded_ip = self.frames[frame_idx].ip;
1784                // Pop the frame and return the value
1785                self.frames.pop();
1786                return Ok(Some(val));
1787            }
1788            Op::TryPropagate => {
1789                // A = dest, B = source register
1790                // If source is Err(...) → early return from current function
1791                // If source is Ok(v) → A = v (unwrap)
1792                // If source is None → early return None
1793                // Otherwise → passthrough
1794                let src = self.stack[base + b as usize].clone();
1795                match &src {
1796                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1797                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1798                            // Unwrap: A = inner value
1799                            self.stack[base + a as usize] = ei.fields[0].clone();
1800                        } else if ei.variant.as_ref() == "Err" {
1801                            // Propagate: return the Err from current function
1802                            self.frames.pop();
1803                            return Ok(Some(src));
1804                        } else {
1805                            self.stack[base + a as usize] = src;
1806                        }
1807                    }
1808                    VmValue::None => {
1809                        // Propagate: return None from current function
1810                        self.frames.pop();
1811                        return Ok(Some(VmValue::None));
1812                    }
1813                    _ => {
1814                        // Passthrough
1815                        self.stack[base + a as usize] = src;
1816                    }
1817                }
1818            }
1819            Op::ExtractField => {
1820                // A = dest, B = source reg, C = field index
1821                // If C has high bit set (C | 0x80), extract rest (sublist from index C & 0x7F)
1822                let source = self.stack[base + b as usize].clone();
1823                let is_rest = (c & 0x80) != 0;
1824                let idx = (c & 0x7F) as usize;
1825                let val = if is_rest {
1826                    // Extract rest as sublist from index idx..
1827                    match &source {
1828                        VmValue::List(l) => {
1829                            if idx < l.len() {
1830                                VmValue::List(Box::new(l[idx..].to_vec()))
1831                            } else {
1832                                VmValue::List(Box::default())
1833                            }
1834                        }
1835                        _ => VmValue::List(Box::default()),
1836                    }
1837                } else {
1838                    match &source {
1839                        VmValue::EnumInstance(ei) => {
1840                            ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1841                        }
1842                        VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1843                        _ => VmValue::None,
1844                    }
1845                };
1846                self.stack[base + a as usize] = val;
1847            }
1848            Op::ExtractNamedField => {
1849                // A = dest, B = source reg, C = field name constant index
1850                let source = self.stack[base + b as usize].clone();
1851                let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1852                    Constant::String(s) => s.clone(),
1853                    _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1854                };
1855                let val = match &source {
1856                    VmValue::StructInstance(s) => s
1857                        .fields
1858                        .iter()
1859                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1860                        .map(|(_, v)| v.clone())
1861                        .unwrap_or(VmValue::None),
1862                    VmValue::Map(m) => m
1863                        .iter()
1864                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1865                        .map(|(_, v)| v.clone())
1866                        .unwrap_or(VmValue::None),
1867                    _ => VmValue::None,
1868                };
1869                self.stack[base + a as usize] = val;
1870            }
1871
1872            // Phase 28: Ownership & Move Semantics
1873            Op::LoadMoved => {
1874                self.stack[base + a as usize] = VmValue::Moved;
1875            }
1876            Op::MakeRef => {
1877                let val = self.stack[base + b as usize].clone();
1878                self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1879            }
1880            Op::ParallelFor => {
1881                // Currently compiled as regular ForIter, this opcode is reserved
1882                // for future rayon-backed parallel iteration.
1883            }
1884            Op::AgentExec => {
1885                #[cfg(feature = "native")]
1886                {
1887                    let result = self.handle_agent_exec(frame_idx, b, c)?;
1888                    self.stack[base + a as usize] = result;
1889                }
1890                #[cfg(not(feature = "native"))]
1891                {
1892                    let _ = (a, b, c, frame_idx);
1893                    return Err(runtime_err("Agents not available in WASM".to_string()));
1894                }
1895            }
1896        }
1897        Ok(None)
1898    }
1899
1900    /// Perform a function call.
1901    fn do_call(
1902        &mut self,
1903        func: VmValue,
1904        caller_base: usize,
1905        func_reg: u8,
1906        args_start: u8,
1907        arg_count: u8,
1908    ) -> Result<(), TlError> {
1909        const MAX_CALL_DEPTH: usize = 512;
1910        if self.frames.len() >= MAX_CALL_DEPTH {
1911            return Err(runtime_err(
1912                "Stack overflow: maximum recursion depth (512) exceeded",
1913            ));
1914        }
1915        match func {
1916            VmValue::Function(closure) => {
1917                let proto = closure.prototype.clone();
1918                let arity = proto.arity as usize;
1919
1920                if arg_count as usize != arity {
1921                    return Err(runtime_err(format!(
1922                        "Expected {} arguments, got {}",
1923                        arity, arg_count
1924                    )));
1925                }
1926
1927                // If this is a generator function, create a Generator instead of executing
1928                if proto.is_generator {
1929                    // Close upvalues for the generator
1930                    let mut closed_upvalues = Vec::new();
1931                    for uv in &closure.upvalues {
1932                        match uv {
1933                            UpvalueRef::Open { stack_index } => {
1934                                let val = self.stack[*stack_index].clone();
1935                                closed_upvalues.push(UpvalueRef::Closed(val));
1936                            }
1937                            UpvalueRef::Closed(v) => {
1938                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1939                            }
1940                        }
1941                    }
1942
1943                    // Build initial saved_stack with args
1944                    let num_regs = proto.num_registers as usize;
1945                    let mut saved_stack = vec![VmValue::None; num_regs];
1946                    for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1947                        *slot = self.stack[caller_base + args_start as usize + i].clone();
1948                    }
1949
1950                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
1951                        prototype: proto,
1952                        upvalues: closed_upvalues,
1953                        saved_stack,
1954                        ip: 0,
1955                    });
1956                    self.stack[caller_base + func_reg as usize] =
1957                        VmValue::Generator(Arc::new(Mutex::new(gn)));
1958                    return Ok(());
1959                }
1960
1961                // Set up new frame
1962                let new_base = self.stack.len();
1963                self.ensure_stack(new_base + proto.num_registers as usize + 1);
1964
1965                // Copy args to new frame's registers
1966                for i in 0..arg_count as usize {
1967                    self.stack[new_base + i] =
1968                        self.stack[caller_base + args_start as usize + i].clone();
1969                }
1970
1971                self.frames.push(CallFrame {
1972                    prototype: proto,
1973                    ip: 0,
1974                    base: new_base,
1975                    upvalues: closure.upvalues.clone(),
1976                });
1977
1978                // Run the function
1979                let result = self.run()?;
1980
1981                // Close any upvalues in the result that point into this frame's stack
1982                let result = self.close_upvalues_in_value(result, new_base);
1983
1984                // Store result in caller's func_reg
1985                self.stack[caller_base + func_reg as usize] = result;
1986
1987                // Shrink stack back
1988                self.stack.truncate(new_base);
1989
1990                Ok(())
1991            }
1992            VmValue::Builtin(builtin_id) => {
1993                let result = self.call_builtin(
1994                    builtin_id as u16,
1995                    caller_base + args_start as usize,
1996                    arg_count as usize,
1997                )?;
1998                self.stack[caller_base + func_reg as usize] = result;
1999                Ok(())
2000            }
2001            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2002        }
2003    }
2004
2005    /// Walk a VmValue and promote any Open upvalues pointing at or above `frame_base`
2006    /// to Closed. This is called on return values before the caller's stack is truncated,
2007    /// so that closures escaping their defining function retain correct captured values.
2008    /// Check if a value may contain functions with open upvalues (recursive).
2009    fn value_may_need_closing(val: &VmValue) -> bool {
2010        match val {
2011            VmValue::Function(_) => true,
2012            VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2013            VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2014            _ => false,
2015        }
2016    }
2017
2018    fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2019        match val {
2020            VmValue::Function(ref closure) => {
2021                // Check if any upvalue needs closing
2022                let needs_closing = closure.upvalues.iter().any(|uv| {
2023                    matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2024                });
2025                if !needs_closing {
2026                    return val;
2027                }
2028                let closed_upvalues: Vec<UpvalueRef> = closure
2029                    .upvalues
2030                    .iter()
2031                    .map(|uv| match uv {
2032                        UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2033                            UpvalueRef::Closed(self.stack[*stack_index].clone())
2034                        }
2035                        other => other.clone(),
2036                    })
2037                    .collect();
2038                VmValue::Function(Arc::new(VmClosure {
2039                    prototype: closure.prototype.clone(),
2040                    upvalues: closed_upvalues,
2041                }))
2042            }
2043            VmValue::List(items) => {
2044                if !items.iter().any(Self::value_may_need_closing) {
2045                    return VmValue::List(items);
2046                }
2047                VmValue::List(Box::new(
2048                    (*items)
2049                        .into_iter()
2050                        .map(|v| self.close_upvalues_in_value(v, frame_base))
2051                        .collect(),
2052                ))
2053            }
2054            VmValue::Map(entries) => {
2055                if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2056                    return VmValue::Map(entries);
2057                }
2058                VmValue::Map(Box::new(
2059                    (*entries)
2060                        .into_iter()
2061                        .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2062                        .collect(),
2063                ))
2064            }
2065            other => other,
2066        }
2067    }
2068
2069    /// Execute a closure (no arguments) in this VM. Used by spawn().
2070    pub(crate) fn execute_closure(
2071        &mut self,
2072        proto: &Arc<Prototype>,
2073        upvalues: &[UpvalueRef],
2074    ) -> Result<VmValue, TlError> {
2075        let base = self.stack.len();
2076        self.ensure_stack(base + proto.num_registers as usize + 1);
2077        self.frames.push(CallFrame {
2078            prototype: proto.clone(),
2079            ip: 0,
2080            base,
2081            upvalues: upvalues.to_vec(),
2082        });
2083        self.run()
2084    }
2085
2086    /// Execute a closure with arguments in this VM. Used by pmap().
2087    pub(crate) fn execute_closure_with_args(
2088        &mut self,
2089        proto: &Arc<Prototype>,
2090        upvalues: &[UpvalueRef],
2091        args: &[VmValue],
2092    ) -> Result<VmValue, TlError> {
2093        let base = self.stack.len();
2094        self.ensure_stack(base + proto.num_registers as usize + 1);
2095        for (i, arg) in args.iter().enumerate() {
2096            self.stack[base + i] = arg.clone();
2097        }
2098        self.frames.push(CallFrame {
2099            prototype: proto.clone(),
2100            ip: 0,
2101            base,
2102            upvalues: upvalues.to_vec(),
2103        });
2104        self.run()
2105    }
2106
2107    fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2108        let frame = &self.frames[frame_idx];
2109        match &frame.prototype.constants[idx as usize] {
2110            Constant::Int(n) => Ok(VmValue::Int(*n)),
2111            Constant::Float(f) => Ok(VmValue::Float(*f)),
2112            Constant::String(s) => Ok(VmValue::String(s.clone())),
2113            Constant::Prototype(p) => {
2114                // Return as a closure with no upvalues
2115                Ok(VmValue::Function(Arc::new(VmClosure {
2116                    prototype: p.clone(),
2117                    upvalues: Vec::new(),
2118                })))
2119            }
2120            Constant::Decimal(s) => {
2121                use std::str::FromStr;
2122                Ok(VmValue::Decimal(
2123                    rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2124                ))
2125            }
2126            Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2127        }
2128    }
2129
2130    fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2131        let frame = &self.frames[frame_idx];
2132        match &frame.prototype.constants[idx as usize] {
2133            Constant::String(s) => Ok(s.clone()),
2134            _ => Err(runtime_err("Expected string constant")),
2135        }
2136    }
2137
2138    // ── Arithmetic helpers ──
2139
2140    fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2141        let left = &self.stack[base + b as usize];
2142        let right = &self.stack[base + c as usize];
2143        match (left, right) {
2144            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2145                .checked_add(*b)
2146                .map(VmValue::Int)
2147                .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2148            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2149            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2150            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2151            (VmValue::String(a), VmValue::String(b)) => {
2152                Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2153            }
2154            #[cfg(feature = "gpu")]
2155            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2156                let a = a.clone();
2157                let b = b.clone();
2158                let ops = self.get_gpu_ops()?;
2159                let result = ops.add(&a, &b).map_err(runtime_err)?;
2160                Ok(VmValue::GpuTensor(Arc::new(result)))
2161            }
2162            #[cfg(feature = "gpu")]
2163            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2164            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2165                let lv = self.stack[base + b as usize].clone();
2166                let rv = self.stack[base + c as usize].clone();
2167                let a = self.ensure_gpu_tensor(&lv)?;
2168                let b_val = self.ensure_gpu_tensor(&rv)?;
2169                let ops = self.get_gpu_ops()?;
2170                let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2171                Ok(VmValue::GpuTensor(Arc::new(result)))
2172            }
2173            #[cfg(feature = "native")]
2174            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2175                let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2176                Ok(VmValue::Tensor(Arc::new(result)))
2177            }
2178            // Decimal arithmetic
2179            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2180            (VmValue::Decimal(a), VmValue::Int(b)) => {
2181                Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2182            }
2183            (VmValue::Int(a), VmValue::Decimal(b)) => {
2184                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2185            }
2186            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2187            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2188            _ => Err(runtime_err(format!(
2189                "Cannot apply `+` to {} and {}",
2190                left.type_name(),
2191                right.type_name()
2192            ))),
2193        }
2194    }
2195
2196    fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2197        let left = &self.stack[base + b as usize];
2198        let right = &self.stack[base + c as usize];
2199        match (left, right) {
2200            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2201                .checked_sub(*b)
2202                .map(VmValue::Int)
2203                .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2204            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2205            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2206            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2207            #[cfg(feature = "gpu")]
2208            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2209                let a = a.clone();
2210                let b = b.clone();
2211                let ops = self.get_gpu_ops()?;
2212                let result = ops.sub(&a, &b).map_err(runtime_err)?;
2213                Ok(VmValue::GpuTensor(Arc::new(result)))
2214            }
2215            #[cfg(feature = "gpu")]
2216            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2217            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2218                let lv = self.stack[base + b as usize].clone();
2219                let rv = self.stack[base + c as usize].clone();
2220                let a = self.ensure_gpu_tensor(&lv)?;
2221                let b_val = self.ensure_gpu_tensor(&rv)?;
2222                let ops = self.get_gpu_ops()?;
2223                let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2224                Ok(VmValue::GpuTensor(Arc::new(result)))
2225            }
2226            #[cfg(feature = "native")]
2227            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2228                let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2229                Ok(VmValue::Tensor(Arc::new(result)))
2230            }
2231            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2232            (VmValue::Decimal(a), VmValue::Int(b)) => {
2233                Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2234            }
2235            (VmValue::Int(a), VmValue::Decimal(b)) => {
2236                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2237            }
2238            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2239            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2240            _ => Err(runtime_err(format!(
2241                "Cannot apply `-` to {} and {}",
2242                left.type_name(),
2243                right.type_name()
2244            ))),
2245        }
2246    }
2247
2248    fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2249        let left = &self.stack[base + b as usize];
2250        let right = &self.stack[base + c as usize];
2251        match (left, right) {
2252            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2253                .checked_mul(*b)
2254                .map(VmValue::Int)
2255                .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2256            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2257            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2258            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2259            (VmValue::String(a), VmValue::Int(b)) => {
2260                if *b < 0 {
2261                    return Err(runtime_err(
2262                        "Cannot repeat string a negative number of times",
2263                    ));
2264                }
2265                if *b > 10_000_000 {
2266                    return Err(runtime_err(
2267                        "String repeat count too large (max 10,000,000)",
2268                    ));
2269                }
2270                Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2271            }
2272            #[cfg(feature = "gpu")]
2273            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2274                let a = a.clone();
2275                let b = b.clone();
2276                let ops = self.get_gpu_ops()?;
2277                let result = ops.mul(&a, &b).map_err(runtime_err)?;
2278                Ok(VmValue::GpuTensor(Arc::new(result)))
2279            }
2280            #[cfg(feature = "gpu")]
2281            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2282            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2283                let lv = self.stack[base + b as usize].clone();
2284                let rv = self.stack[base + c as usize].clone();
2285                let a = self.ensure_gpu_tensor(&lv)?;
2286                let b_val = self.ensure_gpu_tensor(&rv)?;
2287                let ops = self.get_gpu_ops()?;
2288                let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2289                Ok(VmValue::GpuTensor(Arc::new(result)))
2290            }
2291            #[cfg(feature = "gpu")]
2292            (VmValue::GpuTensor(t), VmValue::Float(s))
2293            | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2294                let t = t.clone();
2295                let s = *s;
2296                let ops = self.get_gpu_ops()?;
2297                let result = ops.scale(&t, s as f32);
2298                Ok(VmValue::GpuTensor(Arc::new(result)))
2299            }
2300            #[cfg(feature = "native")]
2301            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2302                let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2303                Ok(VmValue::Tensor(Arc::new(result)))
2304            }
2305            #[cfg(feature = "native")]
2306            (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2307                let result = t.scale(*s);
2308                Ok(VmValue::Tensor(Arc::new(result)))
2309            }
2310            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2311            (VmValue::Decimal(a), VmValue::Int(b)) => {
2312                Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2313            }
2314            (VmValue::Int(a), VmValue::Decimal(b)) => {
2315                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2316            }
2317            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2318            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2319            _ => Err(runtime_err(format!(
2320                "Cannot apply `*` to {} and {}",
2321                left.type_name(),
2322                right.type_name()
2323            ))),
2324        }
2325    }
2326
2327    fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2328        let left = &self.stack[base + b as usize];
2329        let right = &self.stack[base + c as usize];
2330        match (left, right) {
2331            (VmValue::Int(a), VmValue::Int(b)) => {
2332                if *b == 0 {
2333                    return Err(runtime_err("Division by zero"));
2334                }
2335                Ok(VmValue::Int(a / b))
2336            }
2337            (VmValue::Float(a), VmValue::Float(b)) => {
2338                if *b == 0.0 {
2339                    return Err(runtime_err("Division by zero"));
2340                }
2341                Ok(VmValue::Float(a / b))
2342            }
2343            (VmValue::Int(a), VmValue::Float(b)) => {
2344                if *b == 0.0 {
2345                    return Err(runtime_err("Division by zero"));
2346                }
2347                Ok(VmValue::Float(*a as f64 / b))
2348            }
2349            (VmValue::Float(a), VmValue::Int(b)) => {
2350                if *b == 0 {
2351                    return Err(runtime_err("Division by zero"));
2352                }
2353                Ok(VmValue::Float(a / *b as f64))
2354            }
2355            #[cfg(feature = "gpu")]
2356            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2357                let a = a.clone();
2358                let b = b.clone();
2359                let ops = self.get_gpu_ops()?;
2360                let result = ops.div(&a, &b).map_err(runtime_err)?;
2361                Ok(VmValue::GpuTensor(Arc::new(result)))
2362            }
2363            #[cfg(feature = "gpu")]
2364            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2365            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2366                let lv = self.stack[base + b as usize].clone();
2367                let rv = self.stack[base + c as usize].clone();
2368                let a = self.ensure_gpu_tensor(&lv)?;
2369                let b_val = self.ensure_gpu_tensor(&rv)?;
2370                let ops = self.get_gpu_ops()?;
2371                let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2372                Ok(VmValue::GpuTensor(Arc::new(result)))
2373            }
2374            #[cfg(feature = "native")]
2375            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2376                let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2377                Ok(VmValue::Tensor(Arc::new(result)))
2378            }
2379            (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2380                if b.is_zero() {
2381                    return Err(runtime_err("Division by zero"));
2382                }
2383                Ok(VmValue::Decimal(a / b))
2384            }
2385            (VmValue::Decimal(a), VmValue::Int(b)) => {
2386                if *b == 0 {
2387                    return Err(runtime_err("Division by zero"));
2388                }
2389                Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2390            }
2391            (VmValue::Int(a), VmValue::Decimal(b)) => {
2392                if b.is_zero() {
2393                    return Err(runtime_err("Division by zero"));
2394                }
2395                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2396            }
2397            (VmValue::Decimal(a), VmValue::Float(b)) => {
2398                if *b == 0.0 {
2399                    return Err(runtime_err("Division by zero"));
2400                }
2401                Ok(VmValue::Float(decimal_to_f64(a) / b))
2402            }
2403            (VmValue::Float(a), VmValue::Decimal(b)) => {
2404                if b.is_zero() {
2405                    return Err(runtime_err("Division by zero"));
2406                }
2407                Ok(VmValue::Float(a / decimal_to_f64(b)))
2408            }
2409            _ => Err(runtime_err(format!(
2410                "Cannot apply `/` to {} and {}",
2411                left.type_name(),
2412                right.type_name()
2413            ))),
2414        }
2415    }
2416
2417    fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2418        let left = &self.stack[base + b as usize];
2419        let right = &self.stack[base + c as usize];
2420        match (left, right) {
2421            (VmValue::Int(a), VmValue::Int(b)) => {
2422                if *b == 0 {
2423                    return Err(runtime_err("Modulo by zero"));
2424                }
2425                Ok(VmValue::Int(a % b))
2426            }
2427            (VmValue::Float(a), VmValue::Float(b)) => {
2428                if *b == 0.0 {
2429                    return Err(runtime_err("Modulo by zero"));
2430                }
2431                Ok(VmValue::Float(a % b))
2432            }
2433            (VmValue::Int(a), VmValue::Float(b)) => {
2434                if *b == 0.0 {
2435                    return Err(runtime_err("Modulo by zero"));
2436                }
2437                Ok(VmValue::Float(*a as f64 % b))
2438            }
2439            (VmValue::Float(a), VmValue::Int(b)) => {
2440                if *b == 0 {
2441                    return Err(runtime_err("Modulo by zero"));
2442                }
2443                Ok(VmValue::Float(a % *b as f64))
2444            }
2445            _ => Err(runtime_err(format!(
2446                "Cannot apply `%` to {} and {}",
2447                left.type_name(),
2448                right.type_name()
2449            ))),
2450        }
2451    }
2452
2453    fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2454        let left = &self.stack[base + b as usize];
2455        let right = &self.stack[base + c as usize];
2456        match (left, right) {
2457            (VmValue::Int(a), VmValue::Int(b)) => {
2458                if *b < 0 {
2459                    return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2460                }
2461                match a.checked_pow(*b as u32) {
2462                    Some(result) => Ok(VmValue::Int(result)),
2463                    None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2464                }
2465            }
2466            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2467            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2468            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2469            _ => Err(runtime_err(format!(
2470                "Cannot apply `**` to {} and {}",
2471                left.type_name(),
2472                right.type_name()
2473            ))),
2474        }
2475    }
2476
2477    fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2478        self.stack[base + b as usize] == self.stack[base + c as usize]
2479    }
2480
2481    fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2482        let left = &self.stack[base + b as usize];
2483        let right = &self.stack[base + c as usize];
2484        match (left, right) {
2485            (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2486            (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2487            (VmValue::Int(a), VmValue::Float(b)) => {
2488                let fa = *a as f64;
2489                Ok(fa.partial_cmp(b).map(|o| o as i8))
2490            }
2491            (VmValue::Float(a), VmValue::Int(b)) => {
2492                let fb = *b as f64;
2493                Ok(a.partial_cmp(&fb).map(|o| o as i8))
2494            }
2495            (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2496            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2497            (VmValue::Decimal(a), VmValue::Int(b)) => {
2498                Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2499            }
2500            (VmValue::Int(a), VmValue::Decimal(b)) => {
2501                Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2502            }
2503            (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2504            (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2505            (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2506            _ => Err(runtime_err(format!(
2507                "Cannot compare {} and {}",
2508                left.type_name(),
2509                right.type_name()
2510            ))),
2511        }
2512    }
2513
2514    // ── Security helpers ──
2515
2516    fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2517        if let Some(ref policy) = self.security_policy
2518            && !policy.check(perm)
2519        {
2520            return Err(runtime_err(format!("{perm} blocked by security policy")));
2521        }
2522        Ok(())
2523    }
2524
2525    // ── Builtin dispatch ──
2526
2527    pub fn call_builtin(
2528        &mut self,
2529        id: u16,
2530        args_base: usize,
2531        arg_count: usize,
2532    ) -> Result<VmValue, TlError> {
2533        let args: Vec<VmValue> = (0..arg_count)
2534            .map(|i| {
2535                let val = &self.stack[args_base + i];
2536                // Unwrap Ref transparently for builtin calls
2537                match val {
2538                    VmValue::Ref(inner) => inner.as_ref().clone(),
2539                    other => other.clone(),
2540                }
2541            })
2542            .collect();
2543
2544        let builtin_id: BuiltinId =
2545            BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2546
2547        match builtin_id {
2548            BuiltinId::Print | BuiltinId::Println => {
2549                let mut parts = Vec::new();
2550                for a in &args {
2551                    #[cfg(feature = "native")]
2552                    match a {
2553                        VmValue::Table(t) => {
2554                            let batches =
2555                                self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2556                            let formatted =
2557                                DataEngine::format_batches(&batches).map_err(runtime_err)?;
2558                            parts.push(formatted);
2559                        }
2560                        _ => parts.push(format!("{a}")),
2561                    }
2562                    #[cfg(not(feature = "native"))]
2563                    parts.push(format!("{a}"));
2564                }
2565                let line = parts.join(" ");
2566                println!("{line}");
2567                self.output.push(line);
2568                Ok(VmValue::None)
2569            }
2570            BuiltinId::Len => match args.first() {
2571                Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2572                Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2573                Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2574                Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2575                _ => Err(runtime_err("len() expects a string, list, map, or set")),
2576            },
2577            BuiltinId::Str => Ok(VmValue::String(Arc::from(
2578                args.first()
2579                    .map(|v| format!("{v}"))
2580                    .unwrap_or_default()
2581                    .as_str(),
2582            ))),
2583            BuiltinId::Int => match args.first() {
2584                Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2585                Some(VmValue::String(s)) => s
2586                    .parse::<i64>()
2587                    .map(VmValue::Int)
2588                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2589                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2590                Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2591                _ => Err(runtime_err("int() expects a number, string, or bool")),
2592            },
2593            BuiltinId::Float => match args.first() {
2594                Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2595                Some(VmValue::String(s)) => s
2596                    .parse::<f64>()
2597                    .map(VmValue::Float)
2598                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2599                Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2600                Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2601                _ => Err(runtime_err("float() expects a number, string, or bool")),
2602            },
2603            BuiltinId::Abs => match args.first() {
2604                Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2605                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2606                _ => Err(runtime_err("abs() expects a number")),
2607            },
2608            BuiltinId::Min => {
2609                if args.len() == 2 {
2610                    match (&args[0], &args[1]) {
2611                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2612                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2613                        _ => Err(runtime_err("min() expects two numbers")),
2614                    }
2615                } else {
2616                    Err(runtime_err("min() expects 2 arguments"))
2617                }
2618            }
2619            BuiltinId::Max => {
2620                if args.len() == 2 {
2621                    match (&args[0], &args[1]) {
2622                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2623                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2624                        _ => Err(runtime_err("max() expects two numbers")),
2625                    }
2626                } else {
2627                    Err(runtime_err("max() expects 2 arguments"))
2628                }
2629            }
2630            BuiltinId::Range => {
2631                if args.len() == 1 {
2632                    if let VmValue::Int(n) = &args[0] {
2633                        if *n > 10_000_000 {
2634                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2635                        }
2636                        if *n < 0 {
2637                            return Ok(VmValue::List(Box::default()));
2638                        }
2639                        Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2640                    } else {
2641                        Err(runtime_err("range() expects an integer"))
2642                    }
2643                } else if args.len() == 2 {
2644                    if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2645                        let size = (*end - *start).max(0);
2646                        if size > 10_000_000 {
2647                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2648                        }
2649                        Ok(VmValue::List(Box::new(
2650                            (*start..*end).map(VmValue::Int).collect(),
2651                        )))
2652                    } else {
2653                        Err(runtime_err("range() expects integers"))
2654                    }
2655                } else if args.len() == 3 {
2656                    if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2657                        (&args[0], &args[1], &args[2])
2658                    {
2659                        if *step == 0 {
2660                            return Err(runtime_err("range() step cannot be zero"));
2661                        }
2662                        let mut result = Vec::new();
2663                        let mut i = *start;
2664                        if *step > 0 {
2665                            while i < *end {
2666                                result.push(VmValue::Int(i));
2667                                i += step;
2668                            }
2669                        } else {
2670                            while i > *end {
2671                                result.push(VmValue::Int(i));
2672                                i += step;
2673                            }
2674                        }
2675                        Ok(VmValue::List(Box::new(result)))
2676                    } else {
2677                        Err(runtime_err("range() expects integers"))
2678                    }
2679                } else {
2680                    Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2681                }
2682            }
2683            BuiltinId::Push => {
2684                if args.len() == 2 {
2685                    if let VmValue::List(mut items) = args[0].clone() {
2686                        items.push(args[1].clone());
2687                        Ok(VmValue::List(items))
2688                    } else {
2689                        Err(runtime_err("push() first arg must be a list"))
2690                    }
2691                } else {
2692                    Err(runtime_err("push() expects 2 arguments"))
2693                }
2694            }
2695            BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2696                args.first().map(|v| v.type_name()).unwrap_or("none"),
2697            ))),
2698            BuiltinId::Map => {
2699                if args.len() != 2 {
2700                    return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2701                }
2702                let items = match &args[0] {
2703                    VmValue::List(items) => (**items).clone(),
2704                    _ => return Err(runtime_err("map() first arg must be a list")),
2705                };
2706                let func = args[1].clone();
2707                // Parallel path for large lists with pure functions
2708                #[cfg(feature = "native")]
2709                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2710                    let proto = match &func {
2711                        VmValue::Function(c) => c.prototype.clone(),
2712                        _ => unreachable!(),
2713                    };
2714                    let result: Result<Vec<VmValue>, TlError> = items
2715                        .into_par_iter()
2716                        .map(|item| execute_pure_fn(&proto, &[item]))
2717                        .collect();
2718                    return Ok(VmValue::List(Box::new(result?)));
2719                }
2720                let mut result = Vec::new();
2721                for item in items {
2722                    let val = self.call_vm_function(&func, &[item])?;
2723                    result.push(val);
2724                }
2725                Ok(VmValue::List(Box::new(result)))
2726            }
2727            BuiltinId::Filter => {
2728                if args.len() != 2 {
2729                    return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2730                }
2731                let items = match &args[0] {
2732                    VmValue::List(items) => (**items).clone(),
2733                    _ => return Err(runtime_err("filter() first arg must be a list")),
2734                };
2735                let func = args[1].clone();
2736                // Parallel path for large lists with pure functions
2737                #[cfg(feature = "native")]
2738                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2739                    let proto = match &func {
2740                        VmValue::Function(c) => c.prototype.clone(),
2741                        _ => unreachable!(),
2742                    };
2743                    let result: Result<Vec<VmValue>, TlError> = items
2744                        .into_par_iter()
2745                        .filter_map(|item| {
2746                            match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2747                                Ok(val) => {
2748                                    if val.is_truthy() {
2749                                        Some(Ok(item))
2750                                    } else {
2751                                        None
2752                                    }
2753                                }
2754                                Err(e) => Some(Err(e)),
2755                            }
2756                        })
2757                        .collect();
2758                    return Ok(VmValue::List(Box::new(result?)));
2759                }
2760                let mut result = Vec::new();
2761                for item in items {
2762                    let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2763                    if val.is_truthy() {
2764                        result.push(item);
2765                    }
2766                }
2767                Ok(VmValue::List(Box::new(result)))
2768            }
2769            BuiltinId::Reduce | BuiltinId::Fold => {
2770                if args.len() != 3 {
2771                    return Err(runtime_err(
2772                        "reduce()/fold() expects 3 arguments (list, init, fn)",
2773                    ));
2774                }
2775                let items = match &args[0] {
2776                    VmValue::List(items) => (**items).clone(),
2777                    _ => return Err(runtime_err("reduce() first arg must be a list")),
2778                };
2779                let mut acc = args[1].clone();
2780                let func = args[2].clone();
2781                for item in items {
2782                    acc = self.call_vm_function(&func, &[acc, item])?;
2783                }
2784                Ok(acc)
2785            }
2786            BuiltinId::Sum => {
2787                if args.len() != 1 {
2788                    return Err(runtime_err("sum() expects 1 argument (list)"));
2789                }
2790                let items = match &args[0] {
2791                    VmValue::List(items) => items,
2792                    _ => return Err(runtime_err("sum() expects a list")),
2793                };
2794                // Check if any floats are present
2795                let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2796                #[cfg(feature = "native")]
2797                if items.len() >= PARALLEL_THRESHOLD {
2798                    // Parallel sum for large lists
2799                    if has_float {
2800                        let total: f64 = items
2801                            .par_iter()
2802                            .map(|v| match v {
2803                                VmValue::Int(n) => *n as f64,
2804                                VmValue::Float(n) => *n,
2805                                _ => 0.0,
2806                            })
2807                            .sum();
2808                        return Ok(VmValue::Float(total));
2809                    } else {
2810                        let total: i64 = items
2811                            .par_iter()
2812                            .map(|v| match v {
2813                                VmValue::Int(n) => *n,
2814                                _ => 0,
2815                            })
2816                            .sum();
2817                        return Ok(VmValue::Int(total));
2818                    }
2819                }
2820                // Sequential path for smaller lists
2821                let mut total: i64 = 0;
2822                let mut is_float = false;
2823                let mut total_f: f64 = 0.0;
2824                for item in items.iter() {
2825                    match item {
2826                        VmValue::Int(n) => {
2827                            if is_float {
2828                                total_f += *n as f64;
2829                            } else {
2830                                total += n;
2831                            }
2832                        }
2833                        VmValue::Float(n) => {
2834                            if !is_float {
2835                                total_f = total as f64;
2836                                is_float = true;
2837                            }
2838                            total_f += n;
2839                        }
2840                        _ => return Err(runtime_err("sum() list must contain numbers")),
2841                    }
2842                }
2843                if is_float {
2844                    Ok(VmValue::Float(total_f))
2845                } else {
2846                    Ok(VmValue::Int(total))
2847                }
2848            }
2849            BuiltinId::Any => {
2850                if args.len() != 2 {
2851                    return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2852                }
2853                let items = match &args[0] {
2854                    VmValue::List(items) => (**items).clone(),
2855                    _ => return Err(runtime_err("any() first arg must be a list")),
2856                };
2857                let func = args[1].clone();
2858                for item in items {
2859                    let val = self.call_vm_function(&func, &[item])?;
2860                    if val.is_truthy() {
2861                        return Ok(VmValue::Bool(true));
2862                    }
2863                }
2864                Ok(VmValue::Bool(false))
2865            }
2866            BuiltinId::All => {
2867                if args.len() != 2 {
2868                    return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2869                }
2870                let items = match &args[0] {
2871                    VmValue::List(items) => (**items).clone(),
2872                    _ => return Err(runtime_err("all() first arg must be a list")),
2873                };
2874                let func = args[1].clone();
2875                for item in items {
2876                    let val = self.call_vm_function(&func, &[item])?;
2877                    if !val.is_truthy() {
2878                        return Ok(VmValue::Bool(false));
2879                    }
2880                }
2881                Ok(VmValue::Bool(true))
2882            }
2883            // ── Data engine builtins ──
2884            #[cfg(feature = "native")]
2885            BuiltinId::ReadCsv => {
2886                if args.len() != 1 {
2887                    return Err(runtime_err("read_csv() expects 1 argument (path)"));
2888                }
2889                let path = match &args[0] {
2890                    VmValue::String(s) => s.to_string(),
2891                    _ => return Err(runtime_err("read_csv() path must be a string")),
2892                };
2893                match self.engine().read_csv(&path) {
2894                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2895                    Err(e) => {
2896                        let msg = e.to_string();
2897                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2898                            type_name: Arc::from("DataError"),
2899                            variant: Arc::from("ParseError"),
2900                            fields: vec![
2901                                VmValue::String(Arc::from(msg.as_str())),
2902                                VmValue::String(Arc::from(path.as_str())),
2903                            ],
2904                        })));
2905                        Err(runtime_err(msg))
2906                    }
2907                }
2908            }
2909            #[cfg(feature = "native")]
2910            BuiltinId::ReadParquet => {
2911                if args.len() != 1 {
2912                    return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2913                }
2914                let path = match &args[0] {
2915                    VmValue::String(s) => s.to_string(),
2916                    _ => return Err(runtime_err("read_parquet() path must be a string")),
2917                };
2918                match self.engine().read_parquet(&path) {
2919                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2920                    Err(e) => {
2921                        let msg = e.to_string();
2922                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2923                            type_name: Arc::from("DataError"),
2924                            variant: Arc::from("ParseError"),
2925                            fields: vec![
2926                                VmValue::String(Arc::from(msg.as_str())),
2927                                VmValue::String(Arc::from(path.as_str())),
2928                            ],
2929                        })));
2930                        Err(runtime_err(msg))
2931                    }
2932                }
2933            }
2934            #[cfg(feature = "native")]
2935            BuiltinId::WriteCsv => {
2936                if args.len() != 2 {
2937                    return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2938                }
2939                let df = match &args[0] {
2940                    VmValue::Table(t) => t.df.clone(),
2941                    _ => return Err(runtime_err("write_csv() first arg must be a table")),
2942                };
2943                let path = match &args[1] {
2944                    VmValue::String(s) => s.to_string(),
2945                    _ => return Err(runtime_err("write_csv() path must be a string")),
2946                };
2947                match self.engine().write_csv(df, &path) {
2948                    Ok(_) => Ok(VmValue::None),
2949                    Err(e) => {
2950                        let msg = e.to_string();
2951                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2952                            type_name: Arc::from("DataError"),
2953                            variant: Arc::from("ParseError"),
2954                            fields: vec![
2955                                VmValue::String(Arc::from(msg.as_str())),
2956                                VmValue::String(Arc::from(path.as_str())),
2957                            ],
2958                        })));
2959                        Err(runtime_err(msg))
2960                    }
2961                }
2962            }
2963            #[cfg(feature = "native")]
2964            BuiltinId::WriteParquet => {
2965                if args.len() != 2 {
2966                    return Err(runtime_err(
2967                        "write_parquet() expects 2 arguments (table, path)",
2968                    ));
2969                }
2970                let df = match &args[0] {
2971                    VmValue::Table(t) => t.df.clone(),
2972                    _ => return Err(runtime_err("write_parquet() first arg must be a table")),
2973                };
2974                let path = match &args[1] {
2975                    VmValue::String(s) => s.to_string(),
2976                    _ => return Err(runtime_err("write_parquet() path must be a string")),
2977                };
2978                match self.engine().write_parquet(df, &path) {
2979                    Ok(_) => Ok(VmValue::None),
2980                    Err(e) => {
2981                        let msg = e.to_string();
2982                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2983                            type_name: Arc::from("DataError"),
2984                            variant: Arc::from("ParseError"),
2985                            fields: vec![
2986                                VmValue::String(Arc::from(msg.as_str())),
2987                                VmValue::String(Arc::from(path.as_str())),
2988                            ],
2989                        })));
2990                        Err(runtime_err(msg))
2991                    }
2992                }
2993            }
2994            #[cfg(feature = "native")]
2995            BuiltinId::Collect => {
2996                if args.len() != 1 {
2997                    return Err(runtime_err("collect() expects 1 argument (table)"));
2998                }
2999                let df = match &args[0] {
3000                    VmValue::Table(t) => t.df.clone(),
3001                    _ => return Err(runtime_err("collect() expects a table")),
3002                };
3003                let batches = self.engine().collect(df).map_err(runtime_err)?;
3004                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3005                Ok(VmValue::String(Arc::from(formatted.as_str())))
3006            }
3007            #[cfg(feature = "native")]
3008            BuiltinId::ToRows => {
3009                use tl_data::datafusion::arrow::array::{
3010                    Array, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array,
3011                    LargeStringArray, StringArray, UInt32Array, UInt64Array,
3012                };
3013                if args.len() != 1 {
3014                    return Err(runtime_err("to_rows() expects 1 argument (table)"));
3015                }
3016                let df = match &args[0] {
3017                    VmValue::Table(t) => t.df.clone(),
3018                    _ => return Err(runtime_err("to_rows() expects a table")),
3019                };
3020                let batches = self.engine().collect(df).map_err(runtime_err)?;
3021                let mut rows: Vec<VmValue> = Vec::new();
3022                for batch in &batches {
3023                    let schema = batch.schema();
3024                    let num_rows = batch.num_rows();
3025                    for row_idx in 0..num_rows {
3026                        let mut map: Vec<(Arc<str>, VmValue)> = Vec::new();
3027                        for col_idx in 0..batch.num_columns() {
3028                            let col_name: Arc<str> =
3029                                Arc::from(schema.field(col_idx).name().as_str());
3030                            let col = batch.column(col_idx);
3031                            let val = if col.is_null(row_idx) {
3032                                VmValue::None
3033                            } else if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
3034                                VmValue::Float(arr.value(row_idx))
3035                            } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
3036                                VmValue::Float(arr.value(row_idx) as f64)
3037                            } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
3038                                VmValue::Int(arr.value(row_idx))
3039                            } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
3040                                VmValue::Int(arr.value(row_idx) as i64)
3041                            } else if let Some(arr) = col.as_any().downcast_ref::<UInt64Array>() {
3042                                VmValue::Int(arr.value(row_idx) as i64)
3043                            } else if let Some(arr) = col.as_any().downcast_ref::<UInt32Array>() {
3044                                VmValue::Int(arr.value(row_idx) as i64)
3045                            } else if let Some(arr) = col.as_any().downcast_ref::<StringArray>() {
3046                                VmValue::String(Arc::from(arr.value(row_idx)))
3047                            } else if let Some(arr) =
3048                                col.as_any().downcast_ref::<LargeStringArray>()
3049                            {
3050                                VmValue::String(Arc::from(arr.value(row_idx)))
3051                            } else if let Some(arr) = col.as_any().downcast_ref::<BooleanArray>() {
3052                                VmValue::Bool(arr.value(row_idx))
3053                            } else {
3054                                VmValue::String(Arc::from(
3055                                    format!("{:?}", col.data_type()).as_str(),
3056                                ))
3057                            };
3058                            map.push((col_name, val));
3059                        }
3060                        rows.push(VmValue::Map(Box::new(map)));
3061                    }
3062                }
3063                Ok(VmValue::List(Box::new(rows)))
3064            }
3065            #[cfg(feature = "native")]
3066            BuiltinId::Show => {
3067                let df = match args.first() {
3068                    Some(VmValue::Table(t)) => t.df.clone(),
3069                    _ => return Err(runtime_err("show() expects a table")),
3070                };
3071                let limit = match args.get(1) {
3072                    Some(VmValue::Int(n)) => *n as usize,
3073                    None => 20,
3074                    _ => return Err(runtime_err("show() second arg must be an int")),
3075                };
3076                let limited = df
3077                    .limit(0, Some(limit))
3078                    .map_err(|e| runtime_err(format!("{e}")))?;
3079                let batches = self.engine().collect(limited).map_err(runtime_err)?;
3080                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3081                println!("{formatted}");
3082                self.output.push(formatted);
3083                Ok(VmValue::None)
3084            }
3085            #[cfg(feature = "native")]
3086            BuiltinId::Describe => {
3087                if args.len() != 1 {
3088                    return Err(runtime_err("describe() expects 1 argument (table)"));
3089                }
3090                let df = match &args[0] {
3091                    VmValue::Table(t) => t.df.clone(),
3092                    _ => return Err(runtime_err("describe() expects a table")),
3093                };
3094                let schema = df.schema();
3095                let mut lines = Vec::new();
3096                lines.push("Columns:".to_string());
3097                for (qualifier, field) in schema.iter() {
3098                    let prefix = match qualifier {
3099                        Some(q) => format!("{q}."),
3100                        None => String::new(),
3101                    };
3102                    lines.push(format!(
3103                        "  {}{}: {}",
3104                        prefix,
3105                        field.name(),
3106                        field.data_type()
3107                    ));
3108                }
3109                let output = lines.join("\n");
3110                println!("{output}");
3111                self.output.push(output.clone());
3112                Ok(VmValue::String(Arc::from(output.as_str())))
3113            }
3114            #[cfg(feature = "native")]
3115            BuiltinId::Head => {
3116                if args.is_empty() {
3117                    return Err(runtime_err("head() expects at least 1 argument (table)"));
3118                }
3119                let df = match &args[0] {
3120                    VmValue::Table(t) => t.df.clone(),
3121                    _ => return Err(runtime_err("head() first arg must be a table")),
3122                };
3123                let n = match args.get(1) {
3124                    Some(VmValue::Int(n)) => *n as usize,
3125                    None => 10,
3126                    _ => return Err(runtime_err("head() second arg must be an int")),
3127                };
3128                let limited = df
3129                    .limit(0, Some(n))
3130                    .map_err(|e| runtime_err(format!("{e}")))?;
3131                Ok(VmValue::Table(VmTable { df: limited }))
3132            }
3133            #[cfg(feature = "native")]
3134            BuiltinId::Postgres => {
3135                if args.len() != 2 {
3136                    return Err(runtime_err(
3137                        "postgres() expects 2 arguments (conn_str, table_name)",
3138                    ));
3139                }
3140                let conn_str = match &args[0] {
3141                    VmValue::String(s) => s.to_string(),
3142                    _ => return Err(runtime_err("postgres() conn_str must be a string")),
3143                };
3144                let table_name = match &args[1] {
3145                    VmValue::String(s) => s.to_string(),
3146                    _ => return Err(runtime_err("postgres() table_name must be a string")),
3147                };
3148                let conn_str = resolve_tl_config_connection(&conn_str);
3149                match self.engine().read_postgres(&conn_str, &table_name) {
3150                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
3151                    Err(e) => {
3152                        let msg = e.to_string();
3153                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3154                            type_name: Arc::from("ConnectorError"),
3155                            variant: Arc::from("QueryError"),
3156                            fields: vec![
3157                                VmValue::String(Arc::from(msg.as_str())),
3158                                VmValue::String(Arc::from("postgres")),
3159                            ],
3160                        })));
3161                        Err(runtime_err(msg))
3162                    }
3163                }
3164            }
3165            #[cfg(feature = "native")]
3166            BuiltinId::PostgresQuery => {
3167                if args.len() != 2 {
3168                    return Err(runtime_err(
3169                        "postgres_query() expects 2 arguments (conn_str, query)",
3170                    ));
3171                }
3172                let conn_str = match &args[0] {
3173                    VmValue::String(s) => s.to_string(),
3174                    _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3175                };
3176                let query = match &args[1] {
3177                    VmValue::String(s) => s.to_string(),
3178                    _ => return Err(runtime_err("postgres_query() query must be a string")),
3179                };
3180                let conn_str = resolve_tl_config_connection(&conn_str);
3181                match self
3182                    .engine()
3183                    .query_postgres(&conn_str, &query, "__pg_query_result")
3184                {
3185                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
3186                    Err(e) => {
3187                        let msg = e.to_string();
3188                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3189                            type_name: Arc::from("ConnectorError"),
3190                            variant: Arc::from("QueryError"),
3191                            fields: vec![
3192                                VmValue::String(Arc::from(msg.as_str())),
3193                                VmValue::String(Arc::from("postgres")),
3194                            ],
3195                        })));
3196                        Err(runtime_err(msg))
3197                    }
3198                }
3199            }
3200            BuiltinId::TlConfigResolve => {
3201                if args.len() != 1 {
3202                    return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3203                }
3204                let name = match &args[0] {
3205                    VmValue::String(s) => s.to_string(),
3206                    _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3207                };
3208                let resolved = resolve_tl_config_connection(&name);
3209                Ok(VmValue::String(Arc::from(resolved.as_str())))
3210            }
3211            #[cfg(not(feature = "native"))]
3212            BuiltinId::ReadCsv
3213            | BuiltinId::ReadParquet
3214            | BuiltinId::WriteCsv
3215            | BuiltinId::WriteParquet
3216            | BuiltinId::Collect
3217            | BuiltinId::ToRows
3218            | BuiltinId::Show
3219            | BuiltinId::Describe
3220            | BuiltinId::Head
3221            | BuiltinId::Postgres
3222            | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3223            // ── AI builtins ──
3224            #[cfg(feature = "native")]
3225            BuiltinId::Tensor => {
3226                if args.is_empty() {
3227                    return Err(runtime_err("tensor() expects at least 1 argument"));
3228                }
3229                let data = self.vmvalue_to_f64_list(&args[0])?;
3230                let shape = if args.len() > 1 {
3231                    self.vmvalue_to_usize_list(&args[1])?
3232                } else {
3233                    vec![data.len()]
3234                };
3235                let t = tl_ai::TlTensor::from_vec(data, &shape)
3236                    .map_err(|e| runtime_err(e.to_string()))?;
3237                Ok(VmValue::Tensor(Arc::new(t)))
3238            }
3239            #[cfg(feature = "native")]
3240            BuiltinId::TensorZeros => {
3241                if args.is_empty() {
3242                    return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3243                }
3244                let shape = self.vmvalue_to_usize_list(&args[0])?;
3245                let t = tl_ai::TlTensor::zeros(&shape);
3246                Ok(VmValue::Tensor(Arc::new(t)))
3247            }
3248            #[cfg(feature = "native")]
3249            BuiltinId::TensorOnes => {
3250                if args.is_empty() {
3251                    return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3252                }
3253                let shape = self.vmvalue_to_usize_list(&args[0])?;
3254                let t = tl_ai::TlTensor::ones(&shape);
3255                Ok(VmValue::Tensor(Arc::new(t)))
3256            }
3257            #[cfg(feature = "native")]
3258            BuiltinId::TensorShape => match args.first() {
3259                Some(VmValue::Tensor(t)) => {
3260                    let shape: Vec<VmValue> =
3261                        t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3262                    Ok(VmValue::List(Box::new(shape)))
3263                }
3264                _ => Err(runtime_err("tensor_shape() expects a tensor")),
3265            },
3266            #[cfg(feature = "native")]
3267            BuiltinId::TensorReshape => {
3268                if args.len() != 2 {
3269                    return Err(runtime_err(
3270                        "tensor_reshape() expects 2 arguments (tensor, shape)",
3271                    ));
3272                }
3273                let t = match &args[0] {
3274                    VmValue::Tensor(t) => (**t).clone(),
3275                    _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3276                };
3277                let shape = self.vmvalue_to_usize_list(&args[1])?;
3278                let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3279                Ok(VmValue::Tensor(Arc::new(reshaped)))
3280            }
3281            #[cfg(feature = "native")]
3282            BuiltinId::TensorTranspose => match args.first() {
3283                Some(VmValue::Tensor(t)) => {
3284                    let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3285                    Ok(VmValue::Tensor(Arc::new(transposed)))
3286                }
3287                _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3288            },
3289            #[cfg(feature = "native")]
3290            BuiltinId::TensorSum => match args.first() {
3291                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3292                _ => Err(runtime_err("tensor_sum() expects a tensor")),
3293            },
3294            #[cfg(feature = "native")]
3295            BuiltinId::TensorMean => match args.first() {
3296                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3297                _ => Err(runtime_err("tensor_mean() expects a tensor")),
3298            },
3299            #[cfg(feature = "native")]
3300            BuiltinId::TensorDot => {
3301                if args.len() != 2 {
3302                    return Err(runtime_err("tensor_dot() expects 2 arguments"));
3303                }
3304                let a_t = match &args[0] {
3305                    VmValue::Tensor(t) => t,
3306                    _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3307                };
3308                let b_t = match &args[1] {
3309                    VmValue::Tensor(t) => t,
3310                    _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3311                };
3312                let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3313                Ok(VmValue::Tensor(Arc::new(result)))
3314            }
3315            #[cfg(feature = "native")]
3316            BuiltinId::Predict => {
3317                if args.len() < 2 {
3318                    return Err(runtime_err(
3319                        "predict() expects at least 2 arguments (model, input)",
3320                    ));
3321                }
3322                let model = match &args[0] {
3323                    VmValue::Model(m) => (**m).clone(),
3324                    _ => return Err(runtime_err("predict() first arg must be a model")),
3325                };
3326                let input = match &args[1] {
3327                    VmValue::Tensor(t) => (**t).clone(),
3328                    _ => return Err(runtime_err("predict() second arg must be a tensor")),
3329                };
3330                let result =
3331                    tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3332                Ok(VmValue::Tensor(Arc::new(result)))
3333            }
3334            #[cfg(feature = "native")]
3335            BuiltinId::Similarity => {
3336                if args.len() != 2 {
3337                    return Err(runtime_err("similarity() expects 2 arguments"));
3338                }
3339                let a_t = match &args[0] {
3340                    VmValue::Tensor(t) => t,
3341                    _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3342                };
3343                let b_t = match &args[1] {
3344                    VmValue::Tensor(t) => t,
3345                    _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3346                };
3347                let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3348                Ok(VmValue::Float(sim))
3349            }
3350            #[cfg(feature = "native")]
3351            BuiltinId::AiComplete => {
3352                if args.is_empty() {
3353                    return Err(runtime_err(
3354                        "ai_complete() expects at least 1 argument (prompt)",
3355                    ));
3356                }
3357                let prompt = match &args[0] {
3358                    VmValue::String(s) => s.to_string(),
3359                    _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3360                };
3361                let model = match args.get(1) {
3362                    Some(VmValue::String(s)) => Some(s.to_string()),
3363                    _ => None,
3364                };
3365                let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3366                    .map_err(|e| runtime_err(e.to_string()))?;
3367                Ok(VmValue::String(Arc::from(result.as_str())))
3368            }
3369            #[cfg(feature = "native")]
3370            BuiltinId::AiChat => {
3371                if args.is_empty() {
3372                    return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3373                }
3374                let model = match &args[0] {
3375                    VmValue::String(s) => s.to_string(),
3376                    _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3377                };
3378                let system = match args.get(1) {
3379                    Some(VmValue::String(s)) => Some(s.to_string()),
3380                    _ => None,
3381                };
3382                let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3383                {
3384                    msgs.chunks(2)
3385                        .filter_map(|chunk| {
3386                            if chunk.len() == 2
3387                                && let (VmValue::String(role), VmValue::String(content)) =
3388                                    (&chunk[0], &chunk[1])
3389                            {
3390                                return Some((role.to_string(), content.to_string()));
3391                            }
3392                            None
3393                        })
3394                        .collect()
3395                } else {
3396                    Vec::new()
3397                };
3398                let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3399                    .map_err(|e| runtime_err(e.to_string()))?;
3400                Ok(VmValue::String(Arc::from(result.as_str())))
3401            }
3402            #[cfg(feature = "native")]
3403            BuiltinId::ModelSave => {
3404                if args.len() != 2 {
3405                    return Err(runtime_err(
3406                        "model_save() expects 2 arguments (model, path)",
3407                    ));
3408                }
3409                let model = match &args[0] {
3410                    VmValue::Model(m) => m,
3411                    _ => return Err(runtime_err("model_save() first arg must be a model")),
3412                };
3413                let path = match &args[1] {
3414                    VmValue::String(s) => s.to_string(),
3415                    _ => return Err(runtime_err("model_save() second arg must be a string path")),
3416                };
3417                model
3418                    .save(std::path::Path::new(&path))
3419                    .map_err(|e| runtime_err(e.to_string()))?;
3420                Ok(VmValue::None)
3421            }
3422            #[cfg(feature = "native")]
3423            BuiltinId::ModelLoad => {
3424                if args.is_empty() {
3425                    return Err(runtime_err("model_load() expects 1 argument (path)"));
3426                }
3427                let path = match &args[0] {
3428                    VmValue::String(s) => s.to_string(),
3429                    _ => return Err(runtime_err("model_load() arg must be a string path")),
3430                };
3431                let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3432                    .map_err(|e| runtime_err(e.to_string()))?;
3433                Ok(VmValue::Model(Arc::new(model)))
3434            }
3435            #[cfg(feature = "native")]
3436            BuiltinId::ModelRegister => {
3437                if args.len() != 2 {
3438                    return Err(runtime_err(
3439                        "model_register() expects 2 arguments (name, model)",
3440                    ));
3441                }
3442                let name = match &args[0] {
3443                    VmValue::String(s) => s.to_string(),
3444                    _ => return Err(runtime_err("model_register() first arg must be a string")),
3445                };
3446                let model = match &args[1] {
3447                    VmValue::Model(m) => (**m).clone(),
3448                    _ => return Err(runtime_err("model_register() second arg must be a model")),
3449                };
3450                let registry = tl_ai::ModelRegistry::default_location();
3451                registry
3452                    .register(&name, &model)
3453                    .map_err(|e| runtime_err(e.to_string()))?;
3454                Ok(VmValue::None)
3455            }
3456            #[cfg(feature = "native")]
3457            BuiltinId::ModelList => {
3458                let registry = tl_ai::ModelRegistry::default_location();
3459                let names = registry.list();
3460                let items: Vec<VmValue> = names
3461                    .into_iter()
3462                    .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3463                    .collect();
3464                Ok(VmValue::List(Box::new(items)))
3465            }
3466            #[cfg(feature = "native")]
3467            BuiltinId::ModelGet => {
3468                if args.is_empty() {
3469                    return Err(runtime_err("model_get() expects 1 argument (name)"));
3470                }
3471                let name = match &args[0] {
3472                    VmValue::String(s) => s.to_string(),
3473                    _ => return Err(runtime_err("model_get() arg must be a string")),
3474                };
3475                let registry = tl_ai::ModelRegistry::default_location();
3476                match registry.get(&name) {
3477                    Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3478                    Err(_) => Ok(VmValue::None),
3479                }
3480            }
3481            #[cfg(not(feature = "native"))]
3482            BuiltinId::Tensor
3483            | BuiltinId::TensorZeros
3484            | BuiltinId::TensorOnes
3485            | BuiltinId::TensorShape
3486            | BuiltinId::TensorReshape
3487            | BuiltinId::TensorTranspose
3488            | BuiltinId::TensorSum
3489            | BuiltinId::TensorMean
3490            | BuiltinId::TensorDot
3491            | BuiltinId::Predict
3492            | BuiltinId::Similarity
3493            | BuiltinId::AiComplete
3494            | BuiltinId::AiChat
3495            | BuiltinId::ModelSave
3496            | BuiltinId::ModelLoad
3497            | BuiltinId::ModelRegister
3498            | BuiltinId::ModelList
3499            | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3500            // Streaming builtins
3501            #[cfg(feature = "native")]
3502            BuiltinId::AlertSlack => {
3503                if args.len() < 2 {
3504                    return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3505                }
3506                let url = match &args[0] {
3507                    VmValue::String(s) => s.to_string(),
3508                    _ => return Err(runtime_err("alert_slack: url must be a string")),
3509                };
3510                let msg = format!("{}", args[1]);
3511                tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3512                    .map_err(|e| runtime_err(&e))?;
3513                Ok(VmValue::None)
3514            }
3515            #[cfg(feature = "native")]
3516            BuiltinId::AlertWebhook => {
3517                if args.len() < 2 {
3518                    return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3519                }
3520                let url = match &args[0] {
3521                    VmValue::String(s) => s.to_string(),
3522                    _ => return Err(runtime_err("alert_webhook: url must be a string")),
3523                };
3524                let msg = format!("{}", args[1]);
3525                tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3526                    .map_err(|e| runtime_err(&e))?;
3527                Ok(VmValue::None)
3528            }
3529            #[cfg(feature = "native")]
3530            BuiltinId::Emit => {
3531                if args.is_empty() {
3532                    return Err(runtime_err("emit() requires at least 1 argument"));
3533                }
3534                self.output.push(format!("emit: {}", args[0]));
3535                Ok(args[0].clone())
3536            }
3537            #[cfg(feature = "native")]
3538            BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3539            #[cfg(feature = "native")]
3540            BuiltinId::RunPipeline => {
3541                if args.is_empty() {
3542                    return Err(runtime_err("run_pipeline() requires a pipeline"));
3543                }
3544                if let VmValue::PipelineDef(ref def) = args[0] {
3545                    Ok(VmValue::String(Arc::from(
3546                        format!("Pipeline '{}' triggered", def.name).as_str(),
3547                    )))
3548                } else {
3549                    Err(runtime_err("run_pipeline: argument must be a pipeline"))
3550                }
3551            }
3552            #[cfg(not(feature = "native"))]
3553            BuiltinId::AlertSlack
3554            | BuiltinId::AlertWebhook
3555            | BuiltinId::Emit
3556            | BuiltinId::Lineage
3557            | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3558            // Phase 5: Math builtins
3559            BuiltinId::Sqrt => match args.first() {
3560                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3561                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3562                _ => Err(runtime_err("sqrt() expects a number")),
3563            },
3564            BuiltinId::Pow => {
3565                if args.len() == 2 {
3566                    match (&args[0], &args[1]) {
3567                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3568                        (VmValue::Int(a), VmValue::Int(b)) => {
3569                            Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3570                        }
3571                        (VmValue::Float(a), VmValue::Int(b)) => {
3572                            Ok(VmValue::Float(a.powf(*b as f64)))
3573                        }
3574                        (VmValue::Int(a), VmValue::Float(b)) => {
3575                            Ok(VmValue::Float((*a as f64).powf(*b)))
3576                        }
3577                        _ => Err(runtime_err("pow() expects two numbers")),
3578                    }
3579                } else {
3580                    Err(runtime_err("pow() expects 2 arguments"))
3581                }
3582            }
3583            BuiltinId::Floor => match args.first() {
3584                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3585                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3586                _ => Err(runtime_err("floor() expects a number")),
3587            },
3588            BuiltinId::Ceil => match args.first() {
3589                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3590                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3591                _ => Err(runtime_err("ceil() expects a number")),
3592            },
3593            BuiltinId::Round => match args.first() {
3594                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3595                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3596                _ => Err(runtime_err("round() expects a number")),
3597            },
3598            BuiltinId::Sin => match args.first() {
3599                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3600                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3601                _ => Err(runtime_err("sin() expects a number")),
3602            },
3603            BuiltinId::Cos => match args.first() {
3604                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3605                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3606                _ => Err(runtime_err("cos() expects a number")),
3607            },
3608            BuiltinId::Tan => match args.first() {
3609                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3610                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3611                _ => Err(runtime_err("tan() expects a number")),
3612            },
3613            BuiltinId::Log => match args.first() {
3614                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3615                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3616                _ => Err(runtime_err("log() expects a number")),
3617            },
3618            BuiltinId::Log2 => match args.first() {
3619                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3620                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3621                _ => Err(runtime_err("log2() expects a number")),
3622            },
3623            BuiltinId::Log10 => match args.first() {
3624                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3625                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3626                _ => Err(runtime_err("log10() expects a number")),
3627            },
3628            BuiltinId::Join => {
3629                if args.len() == 2 {
3630                    if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3631                        let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3632                        Ok(VmValue::String(Arc::from(
3633                            parts.join(sep.as_ref()).as_str(),
3634                        )))
3635                    } else {
3636                        Err(runtime_err("join() expects separator and list"))
3637                    }
3638                } else {
3639                    Err(runtime_err("join() expects 2 arguments"))
3640                }
3641            }
3642            #[cfg(feature = "native")]
3643            BuiltinId::HttpGet => {
3644                self.check_permission("network")?;
3645                if args.is_empty() {
3646                    return Err(runtime_err("http_get() expects a URL"));
3647                }
3648                if let VmValue::String(url) = &args[0] {
3649                    match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3650                        Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3651                        Err(e) => {
3652                            let msg = format!("HTTP GET error: {e}");
3653                            self.thrown_value =
3654                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3655                                    type_name: Arc::from("NetworkError"),
3656                                    variant: Arc::from("HttpError"),
3657                                    fields: vec![
3658                                        VmValue::String(Arc::from(msg.as_str())),
3659                                        VmValue::String(url.clone()),
3660                                    ],
3661                                })));
3662                            Err(runtime_err(msg))
3663                        }
3664                    }
3665                } else {
3666                    Err(runtime_err("http_get() expects a string URL"))
3667                }
3668            }
3669            #[cfg(feature = "native")]
3670            BuiltinId::HttpPost => {
3671                self.check_permission("network")?;
3672                if args.len() < 2 {
3673                    return Err(runtime_err("http_post() expects URL and body"));
3674                }
3675                if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3676                    let client = reqwest::blocking::Client::new();
3677                    match client
3678                        .post(url.as_ref())
3679                        .header("Content-Type", "application/json")
3680                        .body(body.to_string())
3681                        .send()
3682                        .and_then(|r| r.text())
3683                    {
3684                        Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
3685                        Err(e) => {
3686                            let msg = format!("HTTP POST error: {e}");
3687                            self.thrown_value =
3688                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3689                                    type_name: Arc::from("NetworkError"),
3690                                    variant: Arc::from("HttpError"),
3691                                    fields: vec![
3692                                        VmValue::String(Arc::from(msg.as_str())),
3693                                        VmValue::String(url.clone()),
3694                                    ],
3695                                })));
3696                            Err(runtime_err(msg))
3697                        }
3698                    }
3699                } else {
3700                    Err(runtime_err("http_post() expects string URL and body"))
3701                }
3702            }
3703            #[cfg(not(feature = "native"))]
3704            BuiltinId::HttpGet | BuiltinId::HttpPost => {
3705                Err(runtime_err("HTTP requests not available in WASM"))
3706            }
3707            BuiltinId::Assert => {
3708                if args.is_empty() {
3709                    return Err(runtime_err("assert() expects at least 1 argument"));
3710                }
3711                if !args[0].is_truthy() {
3712                    let msg = if args.len() > 1 {
3713                        format!("{}", args[1])
3714                    } else {
3715                        "Assertion failed".to_string()
3716                    };
3717                    Err(runtime_err(msg))
3718                } else {
3719                    Ok(VmValue::None)
3720                }
3721            }
3722            BuiltinId::AssertEq => {
3723                if args.len() < 2 {
3724                    return Err(runtime_err("assert_eq() expects 2 arguments"));
3725                }
3726                let eq = match (&args[0], &args[1]) {
3727                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
3728                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
3729                    (VmValue::String(a), VmValue::String(b)) => a == b,
3730                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
3731                    (VmValue::None, VmValue::None) => true,
3732                    _ => false,
3733                };
3734                if !eq {
3735                    Err(runtime_err(format!(
3736                        "Assertion failed: {} != {}",
3737                        args[0], args[1]
3738                    )))
3739                } else {
3740                    Ok(VmValue::None)
3741                }
3742            }
3743            // ── Phase 6: Stdlib & Ecosystem builtins ──
3744            BuiltinId::JsonParse => {
3745                if args.is_empty() {
3746                    return Err(runtime_err("json_parse() expects a string"));
3747                }
3748                if let VmValue::String(s) = &args[0] {
3749                    let json_val: serde_json::Value = serde_json::from_str(s)
3750                        .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
3751                    Ok(vm_json_to_value(&json_val))
3752                } else {
3753                    Err(runtime_err("json_parse() expects a string"))
3754                }
3755            }
3756            BuiltinId::JsonStringify => {
3757                if args.is_empty() {
3758                    return Err(runtime_err("json_stringify() expects a value"));
3759                }
3760                let json = vm_value_to_json(&args[0]);
3761                Ok(VmValue::String(Arc::from(json.to_string().as_str())))
3762            }
3763            BuiltinId::MapFrom => {
3764                if !args.len().is_multiple_of(2) {
3765                    return Err(runtime_err(
3766                        "map_from() expects even number of arguments (key, value pairs)",
3767                    ));
3768                }
3769                let mut pairs = Vec::new();
3770                for chunk in args.chunks(2) {
3771                    let key = match &chunk[0] {
3772                        VmValue::String(s) => s.clone(),
3773                        other => Arc::from(format!("{other}").as_str()),
3774                    };
3775                    pairs.push((key, chunk[1].clone()));
3776                }
3777                Ok(VmValue::Map(Box::new(pairs)))
3778            }
3779            #[cfg(feature = "native")]
3780            BuiltinId::ReadFile => {
3781                self.check_permission("file_read")?;
3782                if args.is_empty() {
3783                    return Err(runtime_err("read_file() expects a path"));
3784                }
3785                if let VmValue::String(path) = &args[0] {
3786                    let content = std::fs::read_to_string(path.as_ref())
3787                        .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
3788                    Ok(VmValue::String(Arc::from(content.as_str())))
3789                } else {
3790                    Err(runtime_err("read_file() expects a string path"))
3791                }
3792            }
3793            #[cfg(feature = "native")]
3794            BuiltinId::WriteFile => {
3795                self.check_permission("file_write")?;
3796                if args.len() < 2 {
3797                    return Err(runtime_err("write_file() expects path and content"));
3798                }
3799                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3800                    std::fs::write(path.as_ref(), content.as_ref())
3801                        .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
3802                    Ok(VmValue::None)
3803                } else {
3804                    Err(runtime_err("write_file() expects string path and content"))
3805                }
3806            }
3807            #[cfg(feature = "native")]
3808            BuiltinId::AppendFile => {
3809                self.check_permission("file_write")?;
3810                if args.len() < 2 {
3811                    return Err(runtime_err("append_file() expects path and content"));
3812                }
3813                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3814                    use std::io::Write;
3815                    let mut file = std::fs::OpenOptions::new()
3816                        .create(true)
3817                        .append(true)
3818                        .open(path.as_ref())
3819                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3820                    file.write_all(content.as_bytes())
3821                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3822                    Ok(VmValue::None)
3823                } else {
3824                    Err(runtime_err("append_file() expects string path and content"))
3825                }
3826            }
3827            #[cfg(feature = "native")]
3828            BuiltinId::FileExists => {
3829                self.check_permission("file_read")?;
3830                if args.is_empty() {
3831                    return Err(runtime_err("file_exists() expects a path"));
3832                }
3833                if let VmValue::String(path) = &args[0] {
3834                    Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
3835                } else {
3836                    Err(runtime_err("file_exists() expects a string path"))
3837                }
3838            }
3839            #[cfg(feature = "native")]
3840            BuiltinId::ListDir => {
3841                self.check_permission("file_read")?;
3842                if args.is_empty() {
3843                    return Err(runtime_err("list_dir() expects a path"));
3844                }
3845                if let VmValue::String(path) = &args[0] {
3846                    let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
3847                        .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
3848                        .filter_map(|e| e.ok())
3849                        .map(|e| {
3850                            VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
3851                        })
3852                        .collect();
3853                    Ok(VmValue::List(Box::new(entries)))
3854                } else {
3855                    Err(runtime_err("list_dir() expects a string path"))
3856                }
3857            }
3858            #[cfg(not(feature = "native"))]
3859            BuiltinId::ReadFile
3860            | BuiltinId::WriteFile
3861            | BuiltinId::AppendFile
3862            | BuiltinId::FileExists
3863            | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
3864            #[cfg(feature = "native")]
3865            BuiltinId::EnvGet => {
3866                if args.is_empty() {
3867                    return Err(runtime_err("env_get() expects a name"));
3868                }
3869                if let VmValue::String(name) = &args[0] {
3870                    match std::env::var(name.as_ref()) {
3871                        Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
3872                        Err(_) => Ok(VmValue::None),
3873                    }
3874                } else {
3875                    Err(runtime_err("env_get() expects a string"))
3876                }
3877            }
3878            #[cfg(feature = "native")]
3879            BuiltinId::EnvSet => {
3880                self.check_permission("env_write")?;
3881                if args.len() < 2 {
3882                    return Err(runtime_err("env_set() expects name and value"));
3883                }
3884                if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
3885                    let _guard = env_lock();
3886                    unsafe {
3887                        std::env::set_var(name.as_ref(), val.as_ref());
3888                    }
3889                    Ok(VmValue::None)
3890                } else {
3891                    Err(runtime_err("env_set() expects two strings"))
3892                }
3893            }
3894            #[cfg(not(feature = "native"))]
3895            BuiltinId::EnvGet | BuiltinId::EnvSet => {
3896                Err(runtime_err("Environment variables not available in WASM"))
3897            }
3898            BuiltinId::RegexMatch => {
3899                if args.len() < 2 {
3900                    return Err(runtime_err("regex_match() expects pattern and string"));
3901                }
3902                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3903                    if pattern.len() > 10_000 {
3904                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3905                    }
3906                    let re = regex::RegexBuilder::new(pattern)
3907                        .size_limit(10_000_000)
3908                        .build()
3909                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3910                    Ok(VmValue::Bool(re.is_match(text)))
3911                } else {
3912                    Err(runtime_err(
3913                        "regex_match() expects string pattern and string",
3914                    ))
3915                }
3916            }
3917            BuiltinId::RegexFind => {
3918                if args.len() < 2 {
3919                    return Err(runtime_err("regex_find() expects pattern and string"));
3920                }
3921                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3922                    if pattern.len() > 10_000 {
3923                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3924                    }
3925                    let re = regex::RegexBuilder::new(pattern)
3926                        .size_limit(10_000_000)
3927                        .build()
3928                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3929                    let matches: Vec<VmValue> = re
3930                        .find_iter(text)
3931                        .map(|m| VmValue::String(Arc::from(m.as_str())))
3932                        .collect();
3933                    Ok(VmValue::List(Box::new(matches)))
3934                } else {
3935                    Err(runtime_err(
3936                        "regex_find() expects string pattern and string",
3937                    ))
3938                }
3939            }
3940            BuiltinId::RegexReplace => {
3941                if args.len() < 3 {
3942                    return Err(runtime_err(
3943                        "regex_replace() expects pattern, string, replacement",
3944                    ));
3945                }
3946                if let (
3947                    VmValue::String(pattern),
3948                    VmValue::String(text),
3949                    VmValue::String(replacement),
3950                ) = (&args[0], &args[1], &args[2])
3951                {
3952                    if pattern.len() > 10_000 {
3953                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3954                    }
3955                    let re = regex::RegexBuilder::new(pattern)
3956                        .size_limit(10_000_000)
3957                        .build()
3958                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3959                    Ok(VmValue::String(Arc::from(
3960                        re.replace_all(text, replacement.as_ref()).as_ref(),
3961                    )))
3962                } else {
3963                    Err(runtime_err("regex_replace() expects three strings"))
3964                }
3965            }
3966            BuiltinId::Now => {
3967                let ts = chrono::Utc::now().timestamp_millis();
3968                Ok(VmValue::DateTime(ts))
3969            }
3970            BuiltinId::DateFormat => {
3971                if args.len() < 2 {
3972                    return Err(runtime_err(
3973                        "date_format() expects datetime/timestamp and format",
3974                    ));
3975                }
3976                let ts = match &args[0] {
3977                    VmValue::DateTime(ms) => *ms,
3978                    VmValue::Int(ms) => *ms,
3979                    _ => {
3980                        return Err(runtime_err(
3981                            "date_format() expects a datetime or int timestamp",
3982                        ));
3983                    }
3984                };
3985                let fmt = match &args[1] {
3986                    VmValue::String(s) => s,
3987                    _ => return Err(runtime_err("date_format() expects a string format")),
3988                };
3989                use chrono::TimeZone;
3990                let secs = ts / 1000;
3991                let nsecs = ((ts % 1000) * 1_000_000) as u32;
3992                let dt = chrono::Utc
3993                    .timestamp_opt(secs, nsecs)
3994                    .single()
3995                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
3996                Ok(VmValue::String(Arc::from(
3997                    dt.format(fmt.as_ref()).to_string().as_str(),
3998                )))
3999            }
4000            BuiltinId::DateParse => {
4001                if args.len() < 2 {
4002                    return Err(runtime_err("date_parse() expects string and format"));
4003                }
4004                if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
4005                    let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
4006                        .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
4007                    let ts = dt.and_utc().timestamp_millis();
4008                    Ok(VmValue::DateTime(ts))
4009                } else {
4010                    Err(runtime_err("date_parse() expects two strings"))
4011                }
4012            }
4013            BuiltinId::Zip => {
4014                if args.len() < 2 {
4015                    return Err(runtime_err("zip() expects two lists"));
4016                }
4017                if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
4018                    let pairs: Vec<VmValue> = a
4019                        .iter()
4020                        .zip(b.iter())
4021                        .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
4022                        .collect();
4023                    Ok(VmValue::List(Box::new(pairs)))
4024                } else {
4025                    Err(runtime_err("zip() expects two lists"))
4026                }
4027            }
4028            BuiltinId::Enumerate => {
4029                if args.is_empty() {
4030                    return Err(runtime_err("enumerate() expects a list"));
4031                }
4032                if let VmValue::List(items) = &args[0] {
4033                    let pairs: Vec<VmValue> = items
4034                        .iter()
4035                        .enumerate()
4036                        .map(|(i, v)| {
4037                            VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
4038                        })
4039                        .collect();
4040                    Ok(VmValue::List(Box::new(pairs)))
4041                } else {
4042                    Err(runtime_err("enumerate() expects a list"))
4043                }
4044            }
4045            BuiltinId::Bool => {
4046                if args.is_empty() {
4047                    return Err(runtime_err("bool() expects a value"));
4048                }
4049                Ok(VmValue::Bool(args[0].is_truthy()))
4050            }
4051
4052            // Phase 7: Concurrency builtins
4053            #[cfg(feature = "native")]
4054            BuiltinId::Spawn => {
4055                if args.is_empty() {
4056                    return Err(runtime_err("spawn() expects a function argument"));
4057                }
4058                match &args[0] {
4059                    VmValue::Function(closure) => {
4060                        let proto = closure.prototype.clone();
4061                        // Close all upvalues (convert Open → Closed with current values)
4062                        let mut closed_upvalues = Vec::new();
4063                        for uv in &closure.upvalues {
4064                            match uv {
4065                                UpvalueRef::Open { stack_index } => {
4066                                    let val = self.stack[*stack_index].clone();
4067                                    closed_upvalues.push(UpvalueRef::Closed(val));
4068                                }
4069                                UpvalueRef::Closed(v) => {
4070                                    closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4071                                }
4072                            }
4073                        }
4074                        let globals = self.globals.clone();
4075                        let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4076
4077                        std::thread::spawn(move || {
4078                            let mut vm = Vm::new();
4079                            vm.globals = globals;
4080                            let result = vm.execute_closure(&proto, &closed_upvalues);
4081                            let _ = tx.send(result.map_err(|e| match e {
4082                                TlError::Runtime(re) => re.message,
4083                                other => format!("{other}"),
4084                            }));
4085                        });
4086
4087                        Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4088                    }
4089                    _ => Err(runtime_err("spawn() expects a function")),
4090                }
4091            }
4092            #[cfg(feature = "native")]
4093            BuiltinId::Sleep => {
4094                if args.is_empty() {
4095                    return Err(runtime_err("sleep() expects a duration in milliseconds"));
4096                }
4097                match &args[0] {
4098                    VmValue::Int(ms) => {
4099                        std::thread::sleep(Duration::from_millis(*ms as u64));
4100                        Ok(VmValue::None)
4101                    }
4102                    _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4103                }
4104            }
4105            #[cfg(feature = "native")]
4106            BuiltinId::Channel => {
4107                let capacity = match args.first() {
4108                    Some(VmValue::Int(n)) => *n as usize,
4109                    None => 64,
4110                    _ => {
4111                        return Err(runtime_err(
4112                            "channel() expects an optional integer capacity",
4113                        ));
4114                    }
4115                };
4116                Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4117            }
4118            #[cfg(feature = "native")]
4119            BuiltinId::Send => {
4120                if args.len() < 2 {
4121                    return Err(runtime_err("send() expects a channel and a value"));
4122                }
4123                match &args[0] {
4124                    VmValue::Channel(ch) => {
4125                        ch.sender
4126                            .send(args[1].clone())
4127                            .map_err(|_| runtime_err("Channel disconnected"))?;
4128                        Ok(VmValue::None)
4129                    }
4130                    _ => Err(runtime_err("send() expects a channel as first argument")),
4131                }
4132            }
4133            #[cfg(feature = "native")]
4134            BuiltinId::Recv => {
4135                if args.is_empty() {
4136                    return Err(runtime_err("recv() expects a channel"));
4137                }
4138                match &args[0] {
4139                    VmValue::Channel(ch) => {
4140                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4141                        match guard.recv() {
4142                            Ok(val) => Ok(val),
4143                            Err(_) => Ok(VmValue::None),
4144                        }
4145                    }
4146                    _ => Err(runtime_err("recv() expects a channel")),
4147                }
4148            }
4149            #[cfg(feature = "native")]
4150            BuiltinId::TryRecv => {
4151                if args.is_empty() {
4152                    return Err(runtime_err("try_recv() expects a channel"));
4153                }
4154                match &args[0] {
4155                    VmValue::Channel(ch) => {
4156                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4157                        match guard.try_recv() {
4158                            Ok(val) => Ok(val),
4159                            Err(_) => Ok(VmValue::None),
4160                        }
4161                    }
4162                    _ => Err(runtime_err("try_recv() expects a channel")),
4163                }
4164            }
4165            #[cfg(feature = "native")]
4166            BuiltinId::AwaitAll => {
4167                if args.is_empty() {
4168                    return Err(runtime_err("await_all() expects a list of tasks"));
4169                }
4170                match &args[0] {
4171                    VmValue::List(tasks) => {
4172                        let mut results = Vec::with_capacity(tasks.len());
4173                        for task in tasks.iter() {
4174                            match task {
4175                                VmValue::Task(t) => {
4176                                    let rx = {
4177                                        let mut guard =
4178                                            t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4179                                        guard.take()
4180                                    };
4181                                    match rx {
4182                                        Some(receiver) => match receiver.recv() {
4183                                            Ok(Ok(val)) => results.push(val),
4184                                            Ok(Err(e)) => return Err(runtime_err(e)),
4185                                            Err(_) => {
4186                                                return Err(runtime_err(
4187                                                    "Task channel disconnected",
4188                                                ));
4189                                            }
4190                                        },
4191                                        None => return Err(runtime_err("Task already awaited")),
4192                                    }
4193                                }
4194                                other => results.push(other.clone()),
4195                            }
4196                        }
4197                        Ok(VmValue::List(Box::new(results)))
4198                    }
4199                    _ => Err(runtime_err("await_all() expects a list")),
4200                }
4201            }
4202            #[cfg(feature = "native")]
4203            BuiltinId::Pmap => {
4204                if args.len() < 2 {
4205                    return Err(runtime_err("pmap() expects a list and a function"));
4206                }
4207                let items = match &args[0] {
4208                    VmValue::List(items) => (**items).clone(),
4209                    _ => return Err(runtime_err("pmap() expects a list as first argument")),
4210                };
4211                let closure = match &args[1] {
4212                    VmValue::Function(c) => c.clone(),
4213                    _ => return Err(runtime_err("pmap() expects a function as second argument")),
4214                };
4215
4216                // Close all upvalues
4217                let mut closed_upvalues = Vec::new();
4218                for uv in &closure.upvalues {
4219                    match uv {
4220                        UpvalueRef::Open { stack_index } => {
4221                            let val = self.stack[*stack_index].clone();
4222                            closed_upvalues.push(UpvalueRef::Closed(val));
4223                        }
4224                        UpvalueRef::Closed(v) => {
4225                            closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4226                        }
4227                    }
4228                }
4229
4230                let proto = closure.prototype.clone();
4231                let globals = self.globals.clone();
4232
4233                // Spawn one thread per item
4234                let mut handles = Vec::with_capacity(items.len());
4235                for item in items {
4236                    let proto = proto.clone();
4237                    let upvalues = closed_upvalues.clone();
4238                    let globals = globals.clone();
4239                    let handle = std::thread::spawn(move || {
4240                        let mut vm = Vm::new();
4241                        vm.globals = globals;
4242                        vm.execute_closure_with_args(&proto, &upvalues, &[item])
4243                            .map_err(|e| match e {
4244                                TlError::Runtime(re) => re.message,
4245                                other => format!("{other}"),
4246                            })
4247                    });
4248                    handles.push(handle);
4249                }
4250
4251                let mut results = Vec::with_capacity(handles.len());
4252                for handle in handles {
4253                    match handle.join() {
4254                        Ok(Ok(val)) => results.push(val),
4255                        Ok(Err(e)) => return Err(runtime_err(e)),
4256                        Err(_) => return Err(runtime_err("pmap() thread panicked")),
4257                    }
4258                }
4259                Ok(VmValue::List(Box::new(results)))
4260            }
4261            #[cfg(feature = "native")]
4262            BuiltinId::Timeout => {
4263                if args.len() < 2 {
4264                    return Err(runtime_err(
4265                        "timeout() expects a task and a duration in milliseconds",
4266                    ));
4267                }
4268                let ms = match &args[1] {
4269                    VmValue::Int(n) => *n as u64,
4270                    _ => return Err(runtime_err("timeout() expects an integer duration")),
4271                };
4272                match &args[0] {
4273                    VmValue::Task(task) => {
4274                        let rx = {
4275                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4276                            guard.take()
4277                        };
4278                        match rx {
4279                            Some(receiver) => {
4280                                match receiver.recv_timeout(Duration::from_millis(ms)) {
4281                                    Ok(Ok(val)) => Ok(val),
4282                                    Ok(Err(e)) => Err(runtime_err(e)),
4283                                    Err(mpsc::RecvTimeoutError::Timeout) => {
4284                                        Err(runtime_err("Task timed out"))
4285                                    }
4286                                    Err(mpsc::RecvTimeoutError::Disconnected) => {
4287                                        Err(runtime_err("Task channel disconnected"))
4288                                    }
4289                                }
4290                            }
4291                            None => Err(runtime_err("Task already awaited")),
4292                        }
4293                    }
4294                    _ => Err(runtime_err("timeout() expects a task as first argument")),
4295                }
4296            }
4297            #[cfg(not(feature = "native"))]
4298            BuiltinId::Spawn
4299            | BuiltinId::Sleep
4300            | BuiltinId::Channel
4301            | BuiltinId::Send
4302            | BuiltinId::Recv
4303            | BuiltinId::TryRecv
4304            | BuiltinId::AwaitAll
4305            | BuiltinId::Pmap
4306            | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4307            // Phase 8: Iterators & Generators
4308            BuiltinId::Next => {
4309                if args.is_empty() {
4310                    return Err(runtime_err("next() expects a generator"));
4311                }
4312                match &args[0] {
4313                    VmValue::Generator(gen_arc) => {
4314                        let g = gen_arc.clone();
4315                        self.generator_next(&g)
4316                    }
4317                    _ => Err(runtime_err("next() expects a generator")),
4318                }
4319            }
4320            BuiltinId::IsGenerator => {
4321                let val = args.first().unwrap_or(&VmValue::None);
4322                Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4323            }
4324            BuiltinId::Iter => {
4325                if args.is_empty() {
4326                    return Err(runtime_err("iter() expects a list"));
4327                }
4328                match &args[0] {
4329                    VmValue::List(items) => {
4330                        let gn = VmGenerator::new(GeneratorKind::ListIter {
4331                            items: (**items).clone(),
4332                            index: 0,
4333                        });
4334                        Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4335                    }
4336                    _ => Err(runtime_err("iter() expects a list")),
4337                }
4338            }
4339            BuiltinId::Take => {
4340                if args.len() < 2 {
4341                    return Err(runtime_err("take() expects a generator and a count"));
4342                }
4343                let gen_arc = match &args[0] {
4344                    VmValue::Generator(g) => g.clone(),
4345                    _ => return Err(runtime_err("take() expects a generator as first argument")),
4346                };
4347                let n = match &args[1] {
4348                    VmValue::Int(n) => *n as usize,
4349                    _ => return Err(runtime_err("take() expects an integer count")),
4350                };
4351                let gn = VmGenerator::new(GeneratorKind::Take {
4352                    source: gen_arc,
4353                    remaining: n,
4354                });
4355                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4356            }
4357            BuiltinId::Skip_ => {
4358                if args.len() < 2 {
4359                    return Err(runtime_err("skip() expects a generator and a count"));
4360                }
4361                let gen_arc = match &args[0] {
4362                    VmValue::Generator(g) => g.clone(),
4363                    _ => return Err(runtime_err("skip() expects a generator as first argument")),
4364                };
4365                let n = match &args[1] {
4366                    VmValue::Int(n) => *n as usize,
4367                    _ => return Err(runtime_err("skip() expects an integer count")),
4368                };
4369                let gn = VmGenerator::new(GeneratorKind::Skip {
4370                    source: gen_arc,
4371                    remaining: n,
4372                });
4373                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4374            }
4375            BuiltinId::GenCollect => {
4376                if args.is_empty() {
4377                    return Err(runtime_err("gen_collect() expects a generator"));
4378                }
4379                match &args[0] {
4380                    VmValue::Generator(gen_arc) => {
4381                        let g = gen_arc.clone();
4382                        let mut items = Vec::new();
4383                        loop {
4384                            let val = self.generator_next(&g)?;
4385                            if matches!(val, VmValue::None) {
4386                                break;
4387                            }
4388                            items.push(val);
4389                        }
4390                        Ok(VmValue::List(Box::new(items)))
4391                    }
4392                    _ => Err(runtime_err("gen_collect() expects a generator")),
4393                }
4394            }
4395            BuiltinId::GenMap => {
4396                if args.len() < 2 {
4397                    return Err(runtime_err("gen_map() expects a generator and a function"));
4398                }
4399                let gen_arc = match &args[0] {
4400                    VmValue::Generator(g) => g.clone(),
4401                    _ => {
4402                        return Err(runtime_err(
4403                            "gen_map() expects a generator as first argument",
4404                        ));
4405                    }
4406                };
4407                let func = args[1].clone();
4408                let gn = VmGenerator::new(GeneratorKind::Map {
4409                    source: gen_arc,
4410                    func,
4411                });
4412                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4413            }
4414            BuiltinId::GenFilter => {
4415                if args.len() < 2 {
4416                    return Err(runtime_err(
4417                        "gen_filter() expects a generator and a function",
4418                    ));
4419                }
4420                let gen_arc = match &args[0] {
4421                    VmValue::Generator(g) => g.clone(),
4422                    _ => {
4423                        return Err(runtime_err(
4424                            "gen_filter() expects a generator as first argument",
4425                        ));
4426                    }
4427                };
4428                let func = args[1].clone();
4429                let gn = VmGenerator::new(GeneratorKind::Filter {
4430                    source: gen_arc,
4431                    func,
4432                });
4433                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4434            }
4435            BuiltinId::Chain => {
4436                if args.len() < 2 {
4437                    return Err(runtime_err("chain() expects two generators"));
4438                }
4439                let first = match &args[0] {
4440                    VmValue::Generator(g) => g.clone(),
4441                    _ => return Err(runtime_err("chain() expects generators")),
4442                };
4443                let second = match &args[1] {
4444                    VmValue::Generator(g) => g.clone(),
4445                    _ => return Err(runtime_err("chain() expects generators")),
4446                };
4447                let gn = VmGenerator::new(GeneratorKind::Chain {
4448                    first,
4449                    second,
4450                    on_second: false,
4451                });
4452                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4453            }
4454            BuiltinId::GenZip => {
4455                if args.len() < 2 {
4456                    return Err(runtime_err("gen_zip() expects two generators"));
4457                }
4458                let first = match &args[0] {
4459                    VmValue::Generator(g) => g.clone(),
4460                    _ => return Err(runtime_err("gen_zip() expects generators")),
4461                };
4462                let second = match &args[1] {
4463                    VmValue::Generator(g) => g.clone(),
4464                    _ => return Err(runtime_err("gen_zip() expects generators")),
4465                };
4466                let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4467                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4468            }
4469            BuiltinId::GenEnumerate => {
4470                if args.is_empty() {
4471                    return Err(runtime_err("gen_enumerate() expects a generator"));
4472                }
4473                let gen_arc = match &args[0] {
4474                    VmValue::Generator(g) => g.clone(),
4475                    _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4476                };
4477                let gn = VmGenerator::new(GeneratorKind::Enumerate {
4478                    source: gen_arc,
4479                    index: 0,
4480                });
4481                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4482            }
4483            // Phase 10: Result builtins
4484            BuiltinId::Ok => {
4485                let val = if args.is_empty() {
4486                    VmValue::None
4487                } else {
4488                    args[0].clone()
4489                };
4490                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4491                    type_name: Arc::from("Result"),
4492                    variant: Arc::from("Ok"),
4493                    fields: vec![val],
4494                })))
4495            }
4496            BuiltinId::Err_ => {
4497                let val = if args.is_empty() {
4498                    VmValue::String(Arc::from("error"))
4499                } else {
4500                    args[0].clone()
4501                };
4502                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4503                    type_name: Arc::from("Result"),
4504                    variant: Arc::from("Err"),
4505                    fields: vec![val],
4506                })))
4507            }
4508            BuiltinId::IsOk => {
4509                if args.is_empty() {
4510                    return Err(runtime_err("is_ok() expects an argument"));
4511                }
4512                match &args[0] {
4513                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4514                        Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4515                    }
4516                    _ => Ok(VmValue::Bool(false)),
4517                }
4518            }
4519            BuiltinId::IsErr => {
4520                if args.is_empty() {
4521                    return Err(runtime_err("is_err() expects an argument"));
4522                }
4523                match &args[0] {
4524                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4525                        Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4526                    }
4527                    _ => Ok(VmValue::Bool(false)),
4528                }
4529            }
4530            BuiltinId::Unwrap => {
4531                if args.is_empty() {
4532                    return Err(runtime_err("unwrap() expects an argument"));
4533                }
4534                match &args[0] {
4535                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4536                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4537                            Ok(ei.fields[0].clone())
4538                        } else if ei.variant.as_ref() == "Err" {
4539                            let msg = if ei.fields.is_empty() {
4540                                "error".to_string()
4541                            } else {
4542                                format!("{}", ei.fields[0])
4543                            };
4544                            Err(runtime_err(format!("unwrap() called on Err({msg})")))
4545                        } else {
4546                            Ok(VmValue::None)
4547                        }
4548                    }
4549                    VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4550                    other => Ok(other.clone()),
4551                }
4552            }
4553            BuiltinId::SetFrom => {
4554                let list = match args.first() {
4555                    Some(VmValue::List(items)) => items,
4556                    _ => return Err(runtime_err("set_from() expects a list")),
4557                };
4558                if list.is_empty() {
4559                    return Ok(VmValue::Set(Box::default()));
4560                }
4561                let mut result = Vec::new();
4562                for item in list.iter() {
4563                    if !result.iter().any(|x| vm_values_equal(x, item)) {
4564                        result.push(item.clone());
4565                    }
4566                }
4567                Ok(VmValue::Set(Box::new(result)))
4568            }
4569            BuiltinId::SetAdd => {
4570                if args.len() < 2 {
4571                    return Err(runtime_err("set_add() expects 2 arguments"));
4572                }
4573                let val = &args[1];
4574                match &args[0] {
4575                    VmValue::Set(items) => {
4576                        let mut new_items = items.clone();
4577                        if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4578                            new_items.push(val.clone());
4579                        }
4580                        Ok(VmValue::Set(new_items))
4581                    }
4582                    _ => Err(runtime_err("set_add() first argument must be a set")),
4583                }
4584            }
4585            BuiltinId::SetRemove => {
4586                if args.len() < 2 {
4587                    return Err(runtime_err("set_remove() expects 2 arguments"));
4588                }
4589                let val = &args[1];
4590                match &args[0] {
4591                    VmValue::Set(items) => {
4592                        let new_items: Vec<VmValue> = items
4593                            .iter()
4594                            .filter(|x| !vm_values_equal(x, val))
4595                            .cloned()
4596                            .collect();
4597                        Ok(VmValue::Set(Box::new(new_items)))
4598                    }
4599                    _ => Err(runtime_err("set_remove() first argument must be a set")),
4600                }
4601            }
4602            BuiltinId::SetContains => {
4603                if args.len() < 2 {
4604                    return Err(runtime_err("set_contains() expects 2 arguments"));
4605                }
4606                let val = &args[1];
4607                match &args[0] {
4608                    VmValue::Set(items) => {
4609                        Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4610                    }
4611                    _ => Err(runtime_err("set_contains() first argument must be a set")),
4612                }
4613            }
4614            BuiltinId::SetUnion => {
4615                if args.len() < 2 {
4616                    return Err(runtime_err("set_union() expects 2 arguments"));
4617                }
4618                match (&args[0], &args[1]) {
4619                    (VmValue::Set(a), VmValue::Set(b)) => {
4620                        let mut result = a.clone();
4621                        for item in b.iter() {
4622                            if !result.iter().any(|x| vm_values_equal(x, item)) {
4623                                result.push(item.clone());
4624                            }
4625                        }
4626                        Ok(VmValue::Set(result))
4627                    }
4628                    _ => Err(runtime_err("set_union() expects two sets")),
4629                }
4630            }
4631            BuiltinId::SetIntersection => {
4632                if args.len() < 2 {
4633                    return Err(runtime_err("set_intersection() expects 2 arguments"));
4634                }
4635                match (&args[0], &args[1]) {
4636                    (VmValue::Set(a), VmValue::Set(b)) => {
4637                        let result: Vec<VmValue> = a
4638                            .iter()
4639                            .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4640                            .cloned()
4641                            .collect();
4642                        Ok(VmValue::Set(Box::new(result)))
4643                    }
4644                    _ => Err(runtime_err("set_intersection() expects two sets")),
4645                }
4646            }
4647            BuiltinId::SetDifference => {
4648                if args.len() < 2 {
4649                    return Err(runtime_err("set_difference() expects 2 arguments"));
4650                }
4651                match (&args[0], &args[1]) {
4652                    (VmValue::Set(a), VmValue::Set(b)) => {
4653                        let result: Vec<VmValue> = a
4654                            .iter()
4655                            .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4656                            .cloned()
4657                            .collect();
4658                        Ok(VmValue::Set(Box::new(result)))
4659                    }
4660                    _ => Err(runtime_err("set_difference() expects two sets")),
4661                }
4662            }
4663
4664            // ── Phase 15: Data Quality & Connectors ──
4665            #[cfg(feature = "native")]
4666            BuiltinId::FillNull => {
4667                if args.len() < 2 {
4668                    return Err(runtime_err(
4669                        "fill_null() expects (table, column, [strategy], [value])",
4670                    ));
4671                }
4672                let df = match &args[0] {
4673                    VmValue::Table(t) => t.df.clone(),
4674                    _ => return Err(runtime_err("fill_null() first arg must be a table")),
4675                };
4676                let column = match &args[1] {
4677                    VmValue::String(s) => s.to_string(),
4678                    _ => return Err(runtime_err("fill_null() column must be a string")),
4679                };
4680                let strategy = if args.len() > 2 {
4681                    match &args[2] {
4682                        VmValue::String(s) => s.to_string(),
4683                        _ => "value".to_string(),
4684                    }
4685                } else {
4686                    "value".to_string()
4687                };
4688                let fill_value = if args.len() > 3 {
4689                    match &args[3] {
4690                        VmValue::Int(n) => Some(*n as f64),
4691                        VmValue::Float(f) => Some(*f),
4692                        _ => None,
4693                    }
4694                } else if args.len() > 2 && strategy == "value" {
4695                    match &args[2] {
4696                        VmValue::Int(n) => {
4697                            return Ok(VmValue::Table(VmTable {
4698                                df: self
4699                                    .engine()
4700                                    .fill_null(df, &column, "value", Some(*n as f64))
4701                                    .map_err(runtime_err)?,
4702                            }));
4703                        }
4704                        VmValue::Float(f) => {
4705                            return Ok(VmValue::Table(VmTable {
4706                                df: self
4707                                    .engine()
4708                                    .fill_null(df, &column, "value", Some(*f))
4709                                    .map_err(runtime_err)?,
4710                            }));
4711                        }
4712                        _ => None,
4713                    }
4714                } else {
4715                    None
4716                };
4717                let result = self
4718                    .engine()
4719                    .fill_null(df, &column, &strategy, fill_value)
4720                    .map_err(runtime_err)?;
4721                Ok(VmValue::Table(VmTable { df: result }))
4722            }
4723            #[cfg(feature = "native")]
4724            BuiltinId::DropNull => {
4725                if args.len() < 2 {
4726                    return Err(runtime_err("drop_null() expects (table, column)"));
4727                }
4728                let df = match &args[0] {
4729                    VmValue::Table(t) => t.df.clone(),
4730                    _ => return Err(runtime_err("drop_null() first arg must be a table")),
4731                };
4732                let column = match &args[1] {
4733                    VmValue::String(s) => s.to_string(),
4734                    _ => return Err(runtime_err("drop_null() column must be a string")),
4735                };
4736                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
4737                Ok(VmValue::Table(VmTable { df: result }))
4738            }
4739            #[cfg(feature = "native")]
4740            BuiltinId::Dedup => {
4741                if args.is_empty() {
4742                    return Err(runtime_err("dedup() expects (table, [columns...])"));
4743                }
4744                let df = match &args[0] {
4745                    VmValue::Table(t) => t.df.clone(),
4746                    _ => return Err(runtime_err("dedup() first arg must be a table")),
4747                };
4748                let columns: Vec<String> = args[1..]
4749                    .iter()
4750                    .filter_map(|a| {
4751                        if let VmValue::String(s) = a {
4752                            Some(s.to_string())
4753                        } else {
4754                            None
4755                        }
4756                    })
4757                    .collect();
4758                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
4759                Ok(VmValue::Table(VmTable { df: result }))
4760            }
4761            #[cfg(feature = "native")]
4762            BuiltinId::Clamp => {
4763                if args.len() < 4 {
4764                    return Err(runtime_err("clamp() expects (table, column, min, max)"));
4765                }
4766                let df = match &args[0] {
4767                    VmValue::Table(t) => t.df.clone(),
4768                    _ => return Err(runtime_err("clamp() first arg must be a table")),
4769                };
4770                let column = match &args[1] {
4771                    VmValue::String(s) => s.to_string(),
4772                    _ => return Err(runtime_err("clamp() column must be a string")),
4773                };
4774                let min_val = match &args[2] {
4775                    VmValue::Int(n) => *n as f64,
4776                    VmValue::Float(f) => *f,
4777                    _ => return Err(runtime_err("clamp() min must be a number")),
4778                };
4779                let max_val = match &args[3] {
4780                    VmValue::Int(n) => *n as f64,
4781                    VmValue::Float(f) => *f,
4782                    _ => return Err(runtime_err("clamp() max must be a number")),
4783                };
4784                let result = self
4785                    .engine()
4786                    .clamp(df, &column, min_val, max_val)
4787                    .map_err(runtime_err)?;
4788                Ok(VmValue::Table(VmTable { df: result }))
4789            }
4790            #[cfg(feature = "native")]
4791            BuiltinId::DataProfile => {
4792                if args.is_empty() {
4793                    return Err(runtime_err("data_profile() expects (table)"));
4794                }
4795                let df = match &args[0] {
4796                    VmValue::Table(t) => t.df.clone(),
4797                    _ => return Err(runtime_err("data_profile() arg must be a table")),
4798                };
4799                let result = self.engine().data_profile(df).map_err(runtime_err)?;
4800                Ok(VmValue::Table(VmTable { df: result }))
4801            }
4802            #[cfg(feature = "native")]
4803            BuiltinId::RowCount => {
4804                if args.is_empty() {
4805                    return Err(runtime_err("row_count() expects (table)"));
4806                }
4807                let df = match &args[0] {
4808                    VmValue::Table(t) => t.df.clone(),
4809                    _ => return Err(runtime_err("row_count() arg must be a table")),
4810                };
4811                let count = self.engine().row_count(df).map_err(runtime_err)?;
4812                Ok(VmValue::Int(count))
4813            }
4814            #[cfg(feature = "native")]
4815            BuiltinId::NullRate => {
4816                if args.len() < 2 {
4817                    return Err(runtime_err("null_rate() expects (table, column)"));
4818                }
4819                let df = match &args[0] {
4820                    VmValue::Table(t) => t.df.clone(),
4821                    _ => return Err(runtime_err("null_rate() first arg must be a table")),
4822                };
4823                let column = match &args[1] {
4824                    VmValue::String(s) => s.to_string(),
4825                    _ => return Err(runtime_err("null_rate() column must be a string")),
4826                };
4827                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
4828                Ok(VmValue::Float(rate))
4829            }
4830            #[cfg(feature = "native")]
4831            BuiltinId::IsUnique => {
4832                if args.len() < 2 {
4833                    return Err(runtime_err("is_unique() expects (table, column)"));
4834                }
4835                let df = match &args[0] {
4836                    VmValue::Table(t) => t.df.clone(),
4837                    _ => return Err(runtime_err("is_unique() first arg must be a table")),
4838                };
4839                let column = match &args[1] {
4840                    VmValue::String(s) => s.to_string(),
4841                    _ => return Err(runtime_err("is_unique() column must be a string")),
4842                };
4843                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
4844                Ok(VmValue::Bool(unique))
4845            }
4846            #[cfg(not(feature = "native"))]
4847            BuiltinId::FillNull
4848            | BuiltinId::DropNull
4849            | BuiltinId::Dedup
4850            | BuiltinId::Clamp
4851            | BuiltinId::DataProfile
4852            | BuiltinId::RowCount
4853            | BuiltinId::NullRate
4854            | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
4855            #[cfg(feature = "native")]
4856            BuiltinId::IsEmail => {
4857                if args.is_empty() {
4858                    return Err(runtime_err("is_email() expects 1 argument"));
4859                }
4860                let s = match &args[0] {
4861                    VmValue::String(s) => s.to_string(),
4862                    _ => return Err(runtime_err("is_email() arg must be a string")),
4863                };
4864                Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
4865            }
4866            #[cfg(feature = "native")]
4867            BuiltinId::IsUrl => {
4868                if args.is_empty() {
4869                    return Err(runtime_err("is_url() expects 1 argument"));
4870                }
4871                let s = match &args[0] {
4872                    VmValue::String(s) => s.to_string(),
4873                    _ => return Err(runtime_err("is_url() arg must be a string")),
4874                };
4875                Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
4876            }
4877            #[cfg(feature = "native")]
4878            BuiltinId::IsPhone => {
4879                if args.is_empty() {
4880                    return Err(runtime_err("is_phone() expects 1 argument"));
4881                }
4882                let s = match &args[0] {
4883                    VmValue::String(s) => s.to_string(),
4884                    _ => return Err(runtime_err("is_phone() arg must be a string")),
4885                };
4886                Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
4887            }
4888            #[cfg(feature = "native")]
4889            BuiltinId::IsBetween => {
4890                if args.len() < 3 {
4891                    return Err(runtime_err("is_between() expects (value, low, high)"));
4892                }
4893                let val = match &args[0] {
4894                    VmValue::Int(n) => *n as f64,
4895                    VmValue::Float(f) => *f,
4896                    _ => return Err(runtime_err("is_between() value must be a number")),
4897                };
4898                let low = match &args[1] {
4899                    VmValue::Int(n) => *n as f64,
4900                    VmValue::Float(f) => *f,
4901                    _ => return Err(runtime_err("is_between() low must be a number")),
4902                };
4903                let high = match &args[2] {
4904                    VmValue::Int(n) => *n as f64,
4905                    VmValue::Float(f) => *f,
4906                    _ => return Err(runtime_err("is_between() high must be a number")),
4907                };
4908                Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
4909            }
4910            #[cfg(feature = "native")]
4911            BuiltinId::Levenshtein => {
4912                if args.len() < 2 {
4913                    return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
4914                }
4915                let a = match &args[0] {
4916                    VmValue::String(s) => s.to_string(),
4917                    _ => return Err(runtime_err("levenshtein() args must be strings")),
4918                };
4919                let b = match &args[1] {
4920                    VmValue::String(s) => s.to_string(),
4921                    _ => return Err(runtime_err("levenshtein() args must be strings")),
4922                };
4923                Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
4924            }
4925            #[cfg(feature = "native")]
4926            BuiltinId::Soundex => {
4927                if args.is_empty() {
4928                    return Err(runtime_err("soundex() expects 1 argument"));
4929                }
4930                let s = match &args[0] {
4931                    VmValue::String(s) => s.to_string(),
4932                    _ => return Err(runtime_err("soundex() arg must be a string")),
4933                };
4934                Ok(VmValue::String(Arc::from(
4935                    tl_data::validate::soundex(&s).as_str(),
4936                )))
4937            }
4938            #[cfg(not(feature = "native"))]
4939            BuiltinId::IsEmail
4940            | BuiltinId::IsUrl
4941            | BuiltinId::IsPhone
4942            | BuiltinId::IsBetween
4943            | BuiltinId::Levenshtein
4944            | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
4945            #[cfg(feature = "native")]
4946            BuiltinId::ReadMysql => {
4947                #[cfg(feature = "mysql")]
4948                {
4949                    if args.len() < 2 {
4950                        return Err(runtime_err("read_mysql() expects (conn_str, query)"));
4951                    }
4952                    let conn_str = match &args[0] {
4953                        VmValue::String(s) => s.to_string(),
4954                        _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
4955                    };
4956                    let query = match &args[1] {
4957                        VmValue::String(s) => s.to_string(),
4958                        _ => return Err(runtime_err("read_mysql() query must be a string")),
4959                    };
4960                    let df = self
4961                        .engine()
4962                        .read_mysql(&conn_str, &query)
4963                        .map_err(runtime_err)?;
4964                    Ok(VmValue::Table(VmTable { df }))
4965                }
4966                #[cfg(not(feature = "mysql"))]
4967                Err(runtime_err("read_mysql() requires the 'mysql' feature"))
4968            }
4969            #[cfg(feature = "native")]
4970            BuiltinId::ReadSqlite => {
4971                #[cfg(feature = "sqlite")]
4972                {
4973                    if args.len() < 2 {
4974                        return Err(runtime_err("read_sqlite() expects (db_path, query)"));
4975                    }
4976                    let db_path = match &args[0] {
4977                        VmValue::String(s) => s.to_string(),
4978                        _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
4979                    };
4980                    let query = match &args[1] {
4981                        VmValue::String(s) => s.to_string(),
4982                        _ => return Err(runtime_err("read_sqlite() query must be a string")),
4983                    };
4984                    let df = self
4985                        .engine()
4986                        .read_sqlite(&db_path, &query)
4987                        .map_err(runtime_err)?;
4988                    Ok(VmValue::Table(VmTable { df }))
4989                }
4990                #[cfg(not(feature = "sqlite"))]
4991                Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
4992            }
4993            #[cfg(feature = "native")]
4994            BuiltinId::WriteSqlite => {
4995                #[cfg(feature = "sqlite")]
4996                {
4997                    if args.len() < 3 {
4998                        return Err(runtime_err(
4999                            "write_sqlite() expects (table, db_path, table_name)",
5000                        ));
5001                    }
5002                    let df = match &args[0] {
5003                        VmValue::Table(t) => t.df.clone(),
5004                        _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
5005                    };
5006                    let db_path = match &args[1] {
5007                        VmValue::String(s) => s.to_string(),
5008                        _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
5009                    };
5010                    let table_name = match &args[2] {
5011                        VmValue::String(s) => s.to_string(),
5012                        _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
5013                    };
5014                    self.engine()
5015                        .write_sqlite(df, &db_path, &table_name)
5016                        .map_err(runtime_err)?;
5017                    Ok(VmValue::None)
5018                }
5019                #[cfg(not(feature = "sqlite"))]
5020                Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
5021            }
5022            #[cfg(feature = "native")]
5023            BuiltinId::ReadDuckDb => {
5024                #[cfg(feature = "duckdb")]
5025                {
5026                    if args.len() < 2 {
5027                        return Err(runtime_err("duckdb() expects (db_path, query)"));
5028                    }
5029                    let db_path = match &args[0] {
5030                        VmValue::String(s) => s.to_string(),
5031                        _ => return Err(runtime_err("duckdb() db_path must be a string")),
5032                    };
5033                    let query = match &args[1] {
5034                        VmValue::String(s) => s.to_string(),
5035                        _ => return Err(runtime_err("duckdb() query must be a string")),
5036                    };
5037                    let df = self
5038                        .engine()
5039                        .read_duckdb(&db_path, &query)
5040                        .map_err(runtime_err)?;
5041                    Ok(VmValue::Table(VmTable { df }))
5042                }
5043                #[cfg(not(feature = "duckdb"))]
5044                Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5045            }
5046            #[cfg(feature = "native")]
5047            BuiltinId::WriteDuckDb => {
5048                #[cfg(feature = "duckdb")]
5049                {
5050                    if args.len() < 3 {
5051                        return Err(runtime_err(
5052                            "write_duckdb() expects (table, db_path, table_name)",
5053                        ));
5054                    }
5055                    let df = match &args[0] {
5056                        VmValue::Table(t) => t.df.clone(),
5057                        _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5058                    };
5059                    let db_path = match &args[1] {
5060                        VmValue::String(s) => s.to_string(),
5061                        _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5062                    };
5063                    let table_name = match &args[2] {
5064                        VmValue::String(s) => s.to_string(),
5065                        _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5066                    };
5067                    self.engine()
5068                        .write_duckdb(df, &db_path, &table_name)
5069                        .map_err(runtime_err)?;
5070                    Ok(VmValue::None)
5071                }
5072                #[cfg(not(feature = "duckdb"))]
5073                Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5074            }
5075            #[cfg(feature = "native")]
5076            BuiltinId::ReadRedshift => {
5077                if args.len() < 2 {
5078                    return Err(runtime_err("redshift() expects (conn_str, query)"));
5079                }
5080                let conn_str = match &args[0] {
5081                    VmValue::String(s) => {
5082                        let s_str = s.to_string();
5083                        resolve_tl_config_connection(&s_str)
5084                    }
5085                    _ => return Err(runtime_err("redshift() conn_str must be a string")),
5086                };
5087                let query = match &args[1] {
5088                    VmValue::String(s) => s.to_string(),
5089                    _ => return Err(runtime_err("redshift() query must be a string")),
5090                };
5091                let df = self
5092                    .engine()
5093                    .read_redshift(&conn_str, &query)
5094                    .map_err(runtime_err)?;
5095                Ok(VmValue::Table(VmTable { df }))
5096            }
5097            #[cfg(feature = "native")]
5098            BuiltinId::ReadMssql => {
5099                #[cfg(feature = "mssql")]
5100                {
5101                    if args.len() < 2 {
5102                        return Err(runtime_err("mssql() expects (conn_str, query)"));
5103                    }
5104                    let conn_str = match &args[0] {
5105                        VmValue::String(s) => {
5106                            let s_str = s.to_string();
5107                            resolve_tl_config_connection(&s_str)
5108                        }
5109                        _ => return Err(runtime_err("mssql() conn_str must be a string")),
5110                    };
5111                    let query = match &args[1] {
5112                        VmValue::String(s) => s.to_string(),
5113                        _ => return Err(runtime_err("mssql() query must be a string")),
5114                    };
5115                    let df = self
5116                        .engine()
5117                        .read_mssql(&conn_str, &query)
5118                        .map_err(runtime_err)?;
5119                    Ok(VmValue::Table(VmTable { df }))
5120                }
5121                #[cfg(not(feature = "mssql"))]
5122                Err(runtime_err("mssql() requires the 'mssql' feature"))
5123            }
5124            #[cfg(feature = "native")]
5125            BuiltinId::ReadSnowflake => {
5126                #[cfg(feature = "snowflake")]
5127                {
5128                    if args.len() < 2 {
5129                        return Err(runtime_err("snowflake() expects (config, query)"));
5130                    }
5131                    let config = match &args[0] {
5132                        VmValue::String(s) => {
5133                            let s_str = s.to_string();
5134                            resolve_tl_config_connection(&s_str)
5135                        }
5136                        _ => return Err(runtime_err("snowflake() config must be a string")),
5137                    };
5138                    let query = match &args[1] {
5139                        VmValue::String(s) => s.to_string(),
5140                        _ => return Err(runtime_err("snowflake() query must be a string")),
5141                    };
5142                    let df = self
5143                        .engine()
5144                        .read_snowflake(&config, &query)
5145                        .map_err(runtime_err)?;
5146                    Ok(VmValue::Table(VmTable { df }))
5147                }
5148                #[cfg(not(feature = "snowflake"))]
5149                Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5150            }
5151            #[cfg(feature = "native")]
5152            BuiltinId::ReadBigQuery => {
5153                #[cfg(feature = "bigquery")]
5154                {
5155                    if args.len() < 2 {
5156                        return Err(runtime_err("bigquery() expects (config, query)"));
5157                    }
5158                    let config = match &args[0] {
5159                        VmValue::String(s) => {
5160                            let s_str = s.to_string();
5161                            resolve_tl_config_connection(&s_str)
5162                        }
5163                        _ => return Err(runtime_err("bigquery() config must be a string")),
5164                    };
5165                    let query = match &args[1] {
5166                        VmValue::String(s) => s.to_string(),
5167                        _ => return Err(runtime_err("bigquery() query must be a string")),
5168                    };
5169                    let df = self
5170                        .engine()
5171                        .read_bigquery(&config, &query)
5172                        .map_err(runtime_err)?;
5173                    Ok(VmValue::Table(VmTable { df }))
5174                }
5175                #[cfg(not(feature = "bigquery"))]
5176                Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5177            }
5178            #[cfg(feature = "native")]
5179            BuiltinId::ReadDatabricks => {
5180                #[cfg(feature = "databricks")]
5181                {
5182                    if args.len() < 2 {
5183                        return Err(runtime_err("databricks() expects (config, query)"));
5184                    }
5185                    let config = match &args[0] {
5186                        VmValue::String(s) => {
5187                            let s_str = s.to_string();
5188                            resolve_tl_config_connection(&s_str)
5189                        }
5190                        _ => return Err(runtime_err("databricks() config must be a string")),
5191                    };
5192                    let query = match &args[1] {
5193                        VmValue::String(s) => s.to_string(),
5194                        _ => return Err(runtime_err("databricks() query must be a string")),
5195                    };
5196                    let df = self
5197                        .engine()
5198                        .read_databricks(&config, &query)
5199                        .map_err(runtime_err)?;
5200                    Ok(VmValue::Table(VmTable { df }))
5201                }
5202                #[cfg(not(feature = "databricks"))]
5203                Err(runtime_err(
5204                    "databricks() requires the 'databricks' feature",
5205                ))
5206            }
5207            #[cfg(feature = "native")]
5208            BuiltinId::ReadClickHouse => {
5209                #[cfg(feature = "clickhouse")]
5210                {
5211                    if args.len() < 2 {
5212                        return Err(runtime_err("clickhouse() expects (url, query)"));
5213                    }
5214                    let url = match &args[0] {
5215                        VmValue::String(s) => {
5216                            let s_str = s.to_string();
5217                            resolve_tl_config_connection(&s_str)
5218                        }
5219                        _ => return Err(runtime_err("clickhouse() url must be a string")),
5220                    };
5221                    let query = match &args[1] {
5222                        VmValue::String(s) => s.to_string(),
5223                        _ => return Err(runtime_err("clickhouse() query must be a string")),
5224                    };
5225                    let df = self
5226                        .engine()
5227                        .read_clickhouse(&url, &query)
5228                        .map_err(runtime_err)?;
5229                    Ok(VmValue::Table(VmTable { df }))
5230                }
5231                #[cfg(not(feature = "clickhouse"))]
5232                Err(runtime_err(
5233                    "clickhouse() requires the 'clickhouse' feature",
5234                ))
5235            }
5236            #[cfg(feature = "native")]
5237            BuiltinId::ReadMongo => {
5238                #[cfg(feature = "mongodb")]
5239                {
5240                    if args.len() < 4 {
5241                        return Err(runtime_err(
5242                            "mongo() expects (conn_str, database, collection, filter_json)",
5243                        ));
5244                    }
5245                    let conn_str = match &args[0] {
5246                        VmValue::String(s) => {
5247                            let s_str = s.to_string();
5248                            resolve_tl_config_connection(&s_str)
5249                        }
5250                        _ => return Err(runtime_err("mongo() conn_str must be a string")),
5251                    };
5252                    let database = match &args[1] {
5253                        VmValue::String(s) => s.to_string(),
5254                        _ => return Err(runtime_err("mongo() database must be a string")),
5255                    };
5256                    let collection = match &args[2] {
5257                        VmValue::String(s) => s.to_string(),
5258                        _ => return Err(runtime_err("mongo() collection must be a string")),
5259                    };
5260                    let filter_json = match &args[3] {
5261                        VmValue::String(s) => s.to_string(),
5262                        _ => return Err(runtime_err("mongo() filter must be a string")),
5263                    };
5264                    let df = self
5265                        .engine()
5266                        .read_mongo(&conn_str, &database, &collection, &filter_json)
5267                        .map_err(runtime_err)?;
5268                    Ok(VmValue::Table(VmTable { df }))
5269                }
5270                #[cfg(not(feature = "mongodb"))]
5271                Err(runtime_err("mongo() requires the 'mongodb' feature"))
5272            }
5273            #[cfg(feature = "native")]
5274            BuiltinId::SftpDownload => {
5275                #[cfg(feature = "sftp")]
5276                {
5277                    if args.len() < 3 {
5278                        return Err(runtime_err(
5279                            "sftp_download() expects (config, remote_path, local_path)",
5280                        ));
5281                    }
5282                    let config = match &args[0] {
5283                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5284                        _ => return Err(runtime_err("sftp_download() config must be a string")),
5285                    };
5286                    let remote = match &args[1] {
5287                        VmValue::String(s) => s.to_string(),
5288                        _ => {
5289                            return Err(runtime_err(
5290                                "sftp_download() remote_path must be a string",
5291                            ));
5292                        }
5293                    };
5294                    let local = match &args[2] {
5295                        VmValue::String(s) => s.to_string(),
5296                        _ => {
5297                            return Err(runtime_err("sftp_download() local_path must be a string"));
5298                        }
5299                    };
5300                    let result = self
5301                        .engine()
5302                        .sftp_download(&config, &remote, &local)
5303                        .map_err(runtime_err)?;
5304                    Ok(VmValue::String(Arc::from(result.as_str())))
5305                }
5306                #[cfg(not(feature = "sftp"))]
5307                Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5308            }
5309            #[cfg(feature = "native")]
5310            BuiltinId::SftpUpload => {
5311                #[cfg(feature = "sftp")]
5312                {
5313                    if args.len() < 3 {
5314                        return Err(runtime_err(
5315                            "sftp_upload() expects (config, local_path, remote_path)",
5316                        ));
5317                    }
5318                    let config = match &args[0] {
5319                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5320                        _ => return Err(runtime_err("sftp_upload() config must be a string")),
5321                    };
5322                    let local = match &args[1] {
5323                        VmValue::String(s) => s.to_string(),
5324                        _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5325                    };
5326                    let remote = match &args[2] {
5327                        VmValue::String(s) => s.to_string(),
5328                        _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5329                    };
5330                    let result = self
5331                        .engine()
5332                        .sftp_upload(&config, &local, &remote)
5333                        .map_err(runtime_err)?;
5334                    Ok(VmValue::String(Arc::from(result.as_str())))
5335                }
5336                #[cfg(not(feature = "sftp"))]
5337                Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5338            }
5339            #[cfg(feature = "native")]
5340            BuiltinId::SftpList => {
5341                #[cfg(feature = "sftp")]
5342                {
5343                    if args.len() < 2 {
5344                        return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5345                    }
5346                    let config = match &args[0] {
5347                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5348                        _ => return Err(runtime_err("sftp_list() config must be a string")),
5349                    };
5350                    let remote = match &args[1] {
5351                        VmValue::String(s) => s.to_string(),
5352                        _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5353                    };
5354                    let df = self
5355                        .engine()
5356                        .sftp_list(&config, &remote)
5357                        .map_err(runtime_err)?;
5358                    Ok(VmValue::Table(VmTable { df }))
5359                }
5360                #[cfg(not(feature = "sftp"))]
5361                Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5362            }
5363            #[cfg(feature = "native")]
5364            BuiltinId::SftpReadCsv => {
5365                #[cfg(feature = "sftp")]
5366                {
5367                    if args.len() < 2 {
5368                        return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5369                    }
5370                    let config = match &args[0] {
5371                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5372                        _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5373                    };
5374                    let remote = match &args[1] {
5375                        VmValue::String(s) => s.to_string(),
5376                        _ => {
5377                            return Err(runtime_err(
5378                                "sftp_read_csv() remote_path must be a string",
5379                            ));
5380                        }
5381                    };
5382                    let df = self
5383                        .engine()
5384                        .sftp_read_csv(&config, &remote)
5385                        .map_err(runtime_err)?;
5386                    Ok(VmValue::Table(VmTable { df }))
5387                }
5388                #[cfg(not(feature = "sftp"))]
5389                Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5390            }
5391            #[cfg(feature = "native")]
5392            BuiltinId::SftpReadParquet => {
5393                #[cfg(feature = "sftp")]
5394                {
5395                    if args.len() < 2 {
5396                        return Err(runtime_err(
5397                            "sftp_read_parquet() expects (config, remote_path)",
5398                        ));
5399                    }
5400                    let config = match &args[0] {
5401                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5402                        _ => {
5403                            return Err(runtime_err("sftp_read_parquet() config must be a string"));
5404                        }
5405                    };
5406                    let remote = match &args[1] {
5407                        VmValue::String(s) => s.to_string(),
5408                        _ => {
5409                            return Err(runtime_err(
5410                                "sftp_read_parquet() remote_path must be a string",
5411                            ));
5412                        }
5413                    };
5414                    let df = self
5415                        .engine()
5416                        .sftp_read_parquet(&config, &remote)
5417                        .map_err(runtime_err)?;
5418                    Ok(VmValue::Table(VmTable { df }))
5419                }
5420                #[cfg(not(feature = "sftp"))]
5421                Err(runtime_err(
5422                    "sftp_read_parquet() requires the 'sftp' feature",
5423                ))
5424            }
5425            #[cfg(feature = "native")]
5426            BuiltinId::RedisConnect => {
5427                #[cfg(feature = "redis")]
5428                {
5429                    if args.is_empty() {
5430                        return Err(runtime_err("redis_connect() expects (url)"));
5431                    }
5432                    let url = match &args[0] {
5433                        VmValue::String(s) => s.to_string(),
5434                        _ => return Err(runtime_err("redis_connect() url must be a string")),
5435                    };
5436                    let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5437                    Ok(VmValue::String(Arc::from(result.as_str())))
5438                }
5439                #[cfg(not(feature = "redis"))]
5440                Err(runtime_err("redis_connect() requires the 'redis' feature"))
5441            }
5442            #[cfg(feature = "native")]
5443            BuiltinId::RedisGet => {
5444                #[cfg(feature = "redis")]
5445                {
5446                    if args.len() < 2 {
5447                        return Err(runtime_err("redis_get() expects (url, key)"));
5448                    }
5449                    let url = match &args[0] {
5450                        VmValue::String(s) => s.to_string(),
5451                        _ => return Err(runtime_err("redis_get() url must be a string")),
5452                    };
5453                    let key = match &args[1] {
5454                        VmValue::String(s) => s.to_string(),
5455                        _ => return Err(runtime_err("redis_get() key must be a string")),
5456                    };
5457                    match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5458                        Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5459                        None => Ok(VmValue::None),
5460                    }
5461                }
5462                #[cfg(not(feature = "redis"))]
5463                Err(runtime_err("redis_get() requires the 'redis' feature"))
5464            }
5465            #[cfg(feature = "native")]
5466            BuiltinId::RedisSet => {
5467                #[cfg(feature = "redis")]
5468                {
5469                    if args.len() < 3 {
5470                        return Err(runtime_err("redis_set() expects (url, key, value)"));
5471                    }
5472                    let url = match &args[0] {
5473                        VmValue::String(s) => s.to_string(),
5474                        _ => return Err(runtime_err("redis_set() url must be a string")),
5475                    };
5476                    let key = match &args[1] {
5477                        VmValue::String(s) => s.to_string(),
5478                        _ => return Err(runtime_err("redis_set() key must be a string")),
5479                    };
5480                    let value = match &args[2] {
5481                        VmValue::String(s) => s.to_string(),
5482                        _ => format!("{}", &args[2]),
5483                    };
5484                    tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5485                    Ok(VmValue::None)
5486                }
5487                #[cfg(not(feature = "redis"))]
5488                Err(runtime_err("redis_set() requires the 'redis' feature"))
5489            }
5490            #[cfg(feature = "native")]
5491            BuiltinId::RedisDel => {
5492                #[cfg(feature = "redis")]
5493                {
5494                    if args.len() < 2 {
5495                        return Err(runtime_err("redis_del() expects (url, key)"));
5496                    }
5497                    let url = match &args[0] {
5498                        VmValue::String(s) => s.to_string(),
5499                        _ => return Err(runtime_err("redis_del() url must be a string")),
5500                    };
5501                    let key = match &args[1] {
5502                        VmValue::String(s) => s.to_string(),
5503                        _ => return Err(runtime_err("redis_del() key must be a string")),
5504                    };
5505                    let deleted =
5506                        tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5507                    Ok(VmValue::Bool(deleted))
5508                }
5509                #[cfg(not(feature = "redis"))]
5510                Err(runtime_err("redis_del() requires the 'redis' feature"))
5511            }
5512            #[cfg(feature = "native")]
5513            BuiltinId::GraphqlQuery => {
5514                if args.len() < 2 {
5515                    return Err(runtime_err(
5516                        "graphql_query() expects (endpoint, query, [variables])",
5517                    ));
5518                }
5519                let endpoint = match &args[0] {
5520                    VmValue::String(s) => s.to_string(),
5521                    _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5522                };
5523                let query = match &args[1] {
5524                    VmValue::String(s) => s.to_string(),
5525                    _ => return Err(runtime_err("graphql_query() query must be a string")),
5526                };
5527                let variables = if args.len() > 2 {
5528                    vm_value_to_json(&args[2])
5529                } else {
5530                    serde_json::Value::Null
5531                };
5532                let mut body = serde_json::Map::new();
5533                body.insert("query".to_string(), serde_json::Value::String(query));
5534                if !variables.is_null() {
5535                    body.insert("variables".to_string(), variables);
5536                }
5537                let client = reqwest::blocking::Client::new();
5538                let resp = client
5539                    .post(&endpoint)
5540                    .header("Content-Type", "application/json")
5541                    .json(&body)
5542                    .send()
5543                    .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5544                let text = resp
5545                    .text()
5546                    .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5547                let json: serde_json::Value = serde_json::from_str(&text)
5548                    .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5549                Ok(vm_json_to_value(&json))
5550            }
5551            #[cfg(feature = "native")]
5552            BuiltinId::RegisterS3 => {
5553                #[cfg(feature = "s3")]
5554                {
5555                    if args.len() < 2 {
5556                        return Err(runtime_err(
5557                            "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5558                        ));
5559                    }
5560                    let bucket = match &args[0] {
5561                        VmValue::String(s) => s.to_string(),
5562                        _ => return Err(runtime_err("register_s3() bucket must be a string")),
5563                    };
5564                    let region = match &args[1] {
5565                        VmValue::String(s) => s.to_string(),
5566                        _ => return Err(runtime_err("register_s3() region must be a string")),
5567                    };
5568                    let access_key = args.get(2).and_then(|v| {
5569                        if let VmValue::String(s) = v {
5570                            Some(s.to_string())
5571                        } else {
5572                            None
5573                        }
5574                    });
5575                    let secret_key = args.get(3).and_then(|v| {
5576                        if let VmValue::String(s) = v {
5577                            Some(s.to_string())
5578                        } else {
5579                            None
5580                        }
5581                    });
5582                    let endpoint = args.get(4).and_then(|v| {
5583                        if let VmValue::String(s) = v {
5584                            Some(s.to_string())
5585                        } else {
5586                            None
5587                        }
5588                    });
5589                    self.engine()
5590                        .register_s3(
5591                            &bucket,
5592                            &region,
5593                            access_key.as_deref(),
5594                            secret_key.as_deref(),
5595                            endpoint.as_deref(),
5596                        )
5597                        .map_err(runtime_err)?;
5598                    Ok(VmValue::None)
5599                }
5600                #[cfg(not(feature = "s3"))]
5601                Err(runtime_err("register_s3() requires the 's3' feature"))
5602            }
5603            #[cfg(not(feature = "native"))]
5604            BuiltinId::ReadMysql
5605            | BuiltinId::ReadSqlite
5606            | BuiltinId::WriteSqlite
5607            | BuiltinId::ReadDuckDb
5608            | BuiltinId::WriteDuckDb
5609            | BuiltinId::ReadRedshift
5610            | BuiltinId::ReadMssql
5611            | BuiltinId::ReadSnowflake
5612            | BuiltinId::ReadBigQuery
5613            | BuiltinId::ReadDatabricks
5614            | BuiltinId::ReadClickHouse
5615            | BuiltinId::ReadMongo
5616            | BuiltinId::SftpDownload
5617            | BuiltinId::SftpUpload
5618            | BuiltinId::SftpList
5619            | BuiltinId::SftpReadCsv
5620            | BuiltinId::SftpReadParquet
5621            | BuiltinId::RedisConnect
5622            | BuiltinId::RedisGet
5623            | BuiltinId::RedisSet
5624            | BuiltinId::RedisDel
5625            | BuiltinId::GraphqlQuery
5626            | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
5627            // Phase 20: Python FFI
5628            BuiltinId::PyImport => {
5629                self.check_permission("python")?;
5630                #[cfg(feature = "python")]
5631                {
5632                    crate::python::py_import_impl(&args)
5633                }
5634                #[cfg(not(feature = "python"))]
5635                Err(runtime_err("py_import() requires the 'python' feature"))
5636            }
5637            BuiltinId::PyCall => {
5638                self.check_permission("python")?;
5639                #[cfg(feature = "python")]
5640                {
5641                    crate::python::py_call_impl(&args)
5642                }
5643                #[cfg(not(feature = "python"))]
5644                Err(runtime_err("py_call() requires the 'python' feature"))
5645            }
5646            BuiltinId::PyEval => {
5647                self.check_permission("python")?;
5648                #[cfg(feature = "python")]
5649                {
5650                    crate::python::py_eval_impl(&args)
5651                }
5652                #[cfg(not(feature = "python"))]
5653                Err(runtime_err("py_eval() requires the 'python' feature"))
5654            }
5655            BuiltinId::PyGetAttr => {
5656                self.check_permission("python")?;
5657                #[cfg(feature = "python")]
5658                {
5659                    crate::python::py_getattr_impl(&args)
5660                }
5661                #[cfg(not(feature = "python"))]
5662                Err(runtime_err("py_getattr() requires the 'python' feature"))
5663            }
5664            BuiltinId::PySetAttr => {
5665                self.check_permission("python")?;
5666                #[cfg(feature = "python")]
5667                {
5668                    crate::python::py_setattr_impl(&args)
5669                }
5670                #[cfg(not(feature = "python"))]
5671                Err(runtime_err("py_setattr() requires the 'python' feature"))
5672            }
5673            BuiltinId::PyToTl => {
5674                #[cfg(feature = "python")]
5675                {
5676                    crate::python::py_to_tl_impl(&args)
5677                }
5678                #[cfg(not(feature = "python"))]
5679                Err(runtime_err("py_to_tl() requires the 'python' feature"))
5680            }
5681
5682            // Phase 21: Schema Evolution builtins
5683            #[cfg(feature = "native")]
5684            BuiltinId::SchemaRegister => {
5685                let name = match args.first() {
5686                    Some(VmValue::String(s)) => s.to_string(),
5687                    _ => {
5688                        return Err(runtime_err(
5689                            "schema_register: first arg must be schema name string",
5690                        ));
5691                    }
5692                };
5693                let version = match args.get(1) {
5694                    Some(VmValue::Int(v)) => *v,
5695                    _ => {
5696                        return Err(runtime_err(
5697                            "schema_register: second arg must be version number",
5698                        ));
5699                    }
5700                };
5701                let fields = match args.get(2) {
5702                    Some(VmValue::Map(pairs)) => {
5703                        let mut arrow_fields = Vec::new();
5704                        for (k, v) in pairs.iter() {
5705                            let fname = k.to_string();
5706                            let ftype = match v {
5707                                VmValue::String(s) => s.to_string(),
5708                                _ => "string".to_string(),
5709                            };
5710                            arrow_fields.push(tl_data::ArrowField::new(
5711                                &fname,
5712                                crate::schema::type_name_to_arrow_pub(&ftype),
5713                                true,
5714                            ));
5715                        }
5716                        arrow_fields
5717                    }
5718                    _ => return Err(runtime_err("schema_register: third arg must be fields map")),
5719                };
5720                let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
5721                self.schema_registry
5722                    .register(
5723                        &name,
5724                        version,
5725                        schema,
5726                        crate::schema::SchemaMetadata::default(),
5727                    )
5728                    .map_err(|e| runtime_err(&e))?;
5729                Ok(VmValue::None)
5730            }
5731            #[cfg(feature = "native")]
5732            BuiltinId::SchemaGet => {
5733                let name = match args.first() {
5734                    Some(VmValue::String(s)) => s.to_string(),
5735                    _ => return Err(runtime_err("schema_get: need name")),
5736                };
5737                let version = match args.get(1) {
5738                    Some(VmValue::Int(v)) => *v,
5739                    _ => return Err(runtime_err("schema_get: need version")),
5740                };
5741                match self.schema_registry.get(&name, version) {
5742                    Some(vs) => {
5743                        let fields: Vec<VmValue> = vs
5744                            .schema
5745                            .fields()
5746                            .iter()
5747                            .map(|f| {
5748                                VmValue::String(std::sync::Arc::from(format!(
5749                                    "{}: {}",
5750                                    f.name(),
5751                                    f.data_type()
5752                                )))
5753                            })
5754                            .collect();
5755                        Ok(VmValue::List(Box::new(fields)))
5756                    }
5757                    None => Ok(VmValue::None),
5758                }
5759            }
5760            #[cfg(feature = "native")]
5761            BuiltinId::SchemaLatest => {
5762                let name = match args.first() {
5763                    Some(VmValue::String(s)) => s.to_string(),
5764                    _ => return Err(runtime_err("schema_latest: need name")),
5765                };
5766                match self.schema_registry.latest(&name) {
5767                    Some(vs) => Ok(VmValue::Int(vs.version)),
5768                    None => Ok(VmValue::None),
5769                }
5770            }
5771            #[cfg(feature = "native")]
5772            BuiltinId::SchemaHistory => {
5773                let name = match args.first() {
5774                    Some(VmValue::String(s)) => s.to_string(),
5775                    _ => return Err(runtime_err("schema_history: need name")),
5776                };
5777                let versions = self.schema_registry.versions(&name);
5778                Ok(VmValue::List(Box::new(
5779                    versions.into_iter().map(VmValue::Int).collect(),
5780                )))
5781            }
5782            #[cfg(feature = "native")]
5783            BuiltinId::SchemaCheck => {
5784                let name = match args.first() {
5785                    Some(VmValue::String(s)) => s.to_string(),
5786                    _ => return Err(runtime_err("schema_check: need name")),
5787                };
5788                let v1 = match args.get(1) {
5789                    Some(VmValue::Int(v)) => *v,
5790                    _ => return Err(runtime_err("schema_check: need v1")),
5791                };
5792                let v2 = match args.get(2) {
5793                    Some(VmValue::Int(v)) => *v,
5794                    _ => return Err(runtime_err("schema_check: need v2")),
5795                };
5796                let mode_str = match args.get(3) {
5797                    Some(VmValue::String(s)) => s.to_string(),
5798                    _ => "backward".to_string(),
5799                };
5800                let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
5801                let issues = self
5802                    .schema_registry
5803                    .check_compatibility(&name, v1, v2, mode);
5804                Ok(VmValue::List(Box::new(
5805                    issues
5806                        .into_iter()
5807                        .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
5808                        .collect(),
5809                )))
5810            }
5811            #[cfg(feature = "native")]
5812            BuiltinId::SchemaDiff => {
5813                let name = match args.first() {
5814                    Some(VmValue::String(s)) => s.to_string(),
5815                    _ => return Err(runtime_err("schema_diff: need name")),
5816                };
5817                let v1 = match args.get(1) {
5818                    Some(VmValue::Int(v)) => *v,
5819                    _ => return Err(runtime_err("schema_diff: need v1")),
5820                };
5821                let v2 = match args.get(2) {
5822                    Some(VmValue::Int(v)) => *v,
5823                    _ => return Err(runtime_err("schema_diff: need v2")),
5824                };
5825                let diffs = self.schema_registry.diff(&name, v1, v2);
5826                Ok(VmValue::List(Box::new(
5827                    diffs
5828                        .into_iter()
5829                        .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
5830                        .collect(),
5831                )))
5832            }
5833            #[cfg(feature = "native")]
5834            BuiltinId::SchemaApplyMigration => {
5835                let name = match args.first() {
5836                    Some(VmValue::String(s)) => s.to_string(),
5837                    _ => return Err(runtime_err("schema_apply_migration: need name")),
5838                };
5839                let from_v = match args.get(1) {
5840                    Some(VmValue::Int(v)) => *v,
5841                    _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
5842                };
5843                let to_v = match args.get(2) {
5844                    Some(VmValue::Int(v)) => *v,
5845                    _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
5846                };
5847                Ok(VmValue::String(std::sync::Arc::from(format!(
5848                    "migration {}:{}->{} applied",
5849                    name, from_v, to_v
5850                ))))
5851            }
5852            #[cfg(feature = "native")]
5853            BuiltinId::SchemaVersions => {
5854                let name = match args.first() {
5855                    Some(VmValue::String(s)) => s.to_string(),
5856                    _ => return Err(runtime_err("schema_versions: need name")),
5857                };
5858                let versions = self.schema_registry.versions(&name);
5859                Ok(VmValue::List(Box::new(
5860                    versions.into_iter().map(VmValue::Int).collect(),
5861                )))
5862            }
5863            #[cfg(feature = "native")]
5864            BuiltinId::SchemaFields => {
5865                let name = match args.first() {
5866                    Some(VmValue::String(s)) => s.to_string(),
5867                    _ => return Err(runtime_err("schema_fields: need name")),
5868                };
5869                let version = match args.get(1) {
5870                    Some(VmValue::Int(v)) => *v,
5871                    _ => return Err(runtime_err("schema_fields: need version")),
5872                };
5873                let fields = self.schema_registry.fields(&name, version);
5874                Ok(VmValue::List(Box::new(
5875                    fields
5876                        .into_iter()
5877                        .map(|(n, t)| {
5878                            VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
5879                        })
5880                        .collect(),
5881                )))
5882            }
5883            #[cfg(not(feature = "native"))]
5884            BuiltinId::SchemaRegister
5885            | BuiltinId::SchemaGet
5886            | BuiltinId::SchemaLatest
5887            | BuiltinId::SchemaHistory
5888            | BuiltinId::SchemaCheck
5889            | BuiltinId::SchemaDiff
5890            | BuiltinId::SchemaApplyMigration
5891            | BuiltinId::SchemaVersions
5892            | BuiltinId::SchemaFields => {
5893                let _ = args;
5894                Err(runtime_err("Schema operations not available in WASM"))
5895            }
5896
5897            // ── Phase 22: Advanced Types ──
5898            BuiltinId::Decimal => {
5899                use std::str::FromStr;
5900                let s = match args.first() {
5901                    Some(VmValue::String(s)) => s.to_string(),
5902                    Some(VmValue::Int(n)) => n.to_string(),
5903                    Some(VmValue::Float(f)) => f.to_string(),
5904                    _ => return Err(runtime_err("decimal(): expected string, int, or float")),
5905                };
5906                let d = rust_decimal::Decimal::from_str(&s)
5907                    .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
5908                Ok(VmValue::Decimal(d))
5909            }
5910
5911            // ── Phase 23: Security ──
5912            BuiltinId::SecretGet => {
5913                let key = match args.first() {
5914                    Some(VmValue::String(s)) => s.to_string(),
5915                    _ => return Err(runtime_err("secret_get: need key")),
5916                };
5917                if let Some(val) = self.secret_vault.get(&key) {
5918                    Ok(VmValue::Secret(Arc::from(val.as_str())))
5919                } else {
5920                    // Fallback to env var TL_SECRET_{KEY}
5921                    let env_key = format!("TL_SECRET_{}", key.to_uppercase());
5922                    match std::env::var(&env_key) {
5923                        Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
5924                        Err(_) => Ok(VmValue::None),
5925                    }
5926                }
5927            }
5928            BuiltinId::SecretSet => {
5929                let key = match args.first() {
5930                    Some(VmValue::String(s)) => s.to_string(),
5931                    _ => return Err(runtime_err("secret_set: need key")),
5932                };
5933                let val = match args.get(1) {
5934                    Some(VmValue::String(s)) => s.to_string(),
5935                    Some(VmValue::Secret(s)) => s.to_string(),
5936                    _ => return Err(runtime_err("secret_set: need value")),
5937                };
5938                self.secret_vault.insert(key, val);
5939                Ok(VmValue::None)
5940            }
5941            BuiltinId::SecretDelete => {
5942                let key = match args.first() {
5943                    Some(VmValue::String(s)) => s.to_string(),
5944                    _ => return Err(runtime_err("secret_delete: need key")),
5945                };
5946                self.secret_vault.remove(&key);
5947                Ok(VmValue::None)
5948            }
5949            BuiltinId::SecretList => {
5950                let keys: Vec<VmValue> = self
5951                    .secret_vault
5952                    .keys()
5953                    .map(|k| VmValue::String(Arc::from(k.as_str())))
5954                    .collect();
5955                Ok(VmValue::List(Box::new(keys)))
5956            }
5957            BuiltinId::CheckPermission => {
5958                let perm = match args.first() {
5959                    Some(VmValue::String(s)) => s.to_string(),
5960                    _ => return Err(runtime_err("check_permission: need permission name")),
5961                };
5962                let allowed = match self.security_policy {
5963                    Some(ref policy) => policy.check(&perm),
5964                    None => true,
5965                };
5966                Ok(VmValue::Bool(allowed))
5967            }
5968            BuiltinId::MaskEmail => {
5969                let email = match args.first() {
5970                    Some(VmValue::String(s)) => s.to_string(),
5971                    _ => return Err(runtime_err("mask_email: need string")),
5972                };
5973                let masked = if let Some(at_pos) = email.find('@') {
5974                    let local = &email[..at_pos];
5975                    let domain = &email[at_pos..];
5976                    if local.len() > 1 {
5977                        format!("{}***{}", &local[..1], domain)
5978                    } else {
5979                        format!("***{domain}")
5980                    }
5981                } else {
5982                    "***".to_string()
5983                };
5984                Ok(VmValue::String(Arc::from(masked.as_str())))
5985            }
5986            BuiltinId::MaskPhone => {
5987                let phone = match args.first() {
5988                    Some(VmValue::String(s)) => s.to_string(),
5989                    _ => return Err(runtime_err("mask_phone: need string")),
5990                };
5991                let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
5992                let masked = if digits.len() >= 4 {
5993                    let last4 = &digits[digits.len() - 4..];
5994                    format!("***-***-{last4}")
5995                } else {
5996                    "***".to_string()
5997                };
5998                Ok(VmValue::String(Arc::from(masked.as_str())))
5999            }
6000            BuiltinId::MaskCreditCard => {
6001                let cc = match args.first() {
6002                    Some(VmValue::String(s)) => s.to_string(),
6003                    _ => return Err(runtime_err("mask_cc: need string")),
6004                };
6005                let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
6006                let masked = if digits.len() >= 4 {
6007                    let last4 = &digits[digits.len() - 4..];
6008                    format!("****-****-****-{last4}")
6009                } else {
6010                    "****-****-****-****".to_string()
6011                };
6012                Ok(VmValue::String(Arc::from(masked.as_str())))
6013            }
6014            BuiltinId::Redact => {
6015                let val = match args.first() {
6016                    Some(v) => format!("{v}"),
6017                    _ => return Err(runtime_err("redact: need value")),
6018                };
6019                let policy = match args.get(1) {
6020                    Some(VmValue::String(s)) => s.to_string(),
6021                    _ => "full".to_string(),
6022                };
6023                let result = match policy.as_str() {
6024                    "full" => "***".to_string(),
6025                    "partial" => {
6026                        if val.len() > 2 {
6027                            format!("{}***{}", &val[..1], &val[val.len() - 1..])
6028                        } else {
6029                            "***".to_string()
6030                        }
6031                    }
6032                    "hash" => {
6033                        use sha2::Digest;
6034                        let hash = sha2::Sha256::digest(val.as_bytes());
6035                        format!("{:x}", hash)
6036                    }
6037                    _ => "***".to_string(),
6038                };
6039                Ok(VmValue::String(Arc::from(result.as_str())))
6040            }
6041            BuiltinId::Hash => {
6042                let val = match args.first() {
6043                    Some(VmValue::String(s)) => s.to_string(),
6044                    _ => return Err(runtime_err("hash: need string")),
6045                };
6046                let algo = match args.get(1) {
6047                    Some(VmValue::String(s)) => s.to_string(),
6048                    _ => "sha256".to_string(),
6049                };
6050                let result = match algo.as_str() {
6051                    "sha256" => {
6052                        use sha2::Digest;
6053                        format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6054                    }
6055                    "sha512" => {
6056                        use sha2::Digest;
6057                        format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6058                    }
6059                    "md5" => {
6060                        use md5::Digest;
6061                        format!("{:x}", md5::Md5::digest(val.as_bytes()))
6062                    }
6063                    _ => {
6064                        return Err(runtime_err(format!(
6065                            "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6066                        )));
6067                    }
6068                };
6069                Ok(VmValue::String(Arc::from(result.as_str())))
6070            }
6071
6072            // ── Phase 25: Async builtins (tokio-backed when async-runtime feature enabled) ──
6073            #[cfg(feature = "async-runtime")]
6074            BuiltinId::AsyncReadFile => {
6075                let rt = self.ensure_runtime();
6076                crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6077            }
6078            #[cfg(feature = "async-runtime")]
6079            BuiltinId::AsyncWriteFile => {
6080                let rt = self.ensure_runtime();
6081                crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6082            }
6083            #[cfg(feature = "async-runtime")]
6084            BuiltinId::AsyncHttpGet => {
6085                let rt = self.ensure_runtime();
6086                crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6087            }
6088            #[cfg(feature = "async-runtime")]
6089            BuiltinId::AsyncHttpPost => {
6090                let rt = self.ensure_runtime();
6091                crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6092            }
6093            #[cfg(feature = "async-runtime")]
6094            BuiltinId::AsyncSleep => {
6095                let rt = self.ensure_runtime();
6096                crate::async_runtime::async_sleep_impl(&rt, &args)
6097            }
6098            #[cfg(feature = "async-runtime")]
6099            BuiltinId::Select => crate::async_runtime::select_impl(&args),
6100            #[cfg(feature = "async-runtime")]
6101            BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6102            #[cfg(feature = "async-runtime")]
6103            BuiltinId::AsyncMap => {
6104                let rt = self.ensure_runtime();
6105                let stack_snapshot = self.stack.clone();
6106                crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6107            }
6108            #[cfg(feature = "async-runtime")]
6109            BuiltinId::AsyncFilter => {
6110                let rt = self.ensure_runtime();
6111                let stack_snapshot = self.stack.clone();
6112                crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6113            }
6114
6115            #[cfg(not(feature = "async-runtime"))]
6116            BuiltinId::AsyncReadFile
6117            | BuiltinId::AsyncWriteFile
6118            | BuiltinId::AsyncHttpGet
6119            | BuiltinId::AsyncHttpPost
6120            | BuiltinId::AsyncSleep
6121            | BuiltinId::Select
6122            | BuiltinId::AsyncMap
6123            | BuiltinId::AsyncFilter
6124            | BuiltinId::RaceAll => Err(runtime_err(format!(
6125                "{}: async builtins require the 'async-runtime' feature",
6126                builtin_id.name()
6127            ))),
6128
6129            // Phase 27: Data Error Hierarchy builtins
6130            BuiltinId::IsError => {
6131                if args.is_empty() {
6132                    return Err(runtime_err("is_error() expects 1 argument"));
6133                }
6134                let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6135                    &*e.type_name == "DataError" ||
6136                    &*e.type_name == "NetworkError" ||
6137                    &*e.type_name == "ConnectorError"
6138                );
6139                Ok(VmValue::Bool(is_err))
6140            }
6141            BuiltinId::ErrorType => {
6142                if args.is_empty() {
6143                    return Err(runtime_err("error_type() expects 1 argument"));
6144                }
6145                match &args[0] {
6146                    VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6147                    _ => Ok(VmValue::None),
6148                }
6149            }
6150
6151            // Phase 32: GPU Tensor Support
6152            #[cfg(feature = "gpu")]
6153            BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6154            #[cfg(not(feature = "gpu"))]
6155            BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6156
6157            #[cfg(feature = "gpu")]
6158            BuiltinId::ToGpu => {
6159                if args.is_empty() {
6160                    return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6161                }
6162                let gt = self.ensure_gpu_tensor(&args[0])?;
6163                Ok(VmValue::GpuTensor(gt))
6164            }
6165            #[cfg(not(feature = "gpu"))]
6166            BuiltinId::ToGpu => Err(runtime_err(
6167                "GPU operations not available. Build with --features gpu",
6168            )),
6169
6170            #[cfg(feature = "gpu")]
6171            BuiltinId::ToCpu => {
6172                if args.is_empty() {
6173                    return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6174                }
6175                match &args[0] {
6176                    VmValue::GpuTensor(gt) => {
6177                        let cpu = gt.to_cpu().map_err(runtime_err)?;
6178                        Ok(VmValue::Tensor(Arc::new(cpu)))
6179                    }
6180                    _ => Err(runtime_err(format!(
6181                        "to_cpu() expects a gpu_tensor, got {}",
6182                        args[0].type_name()
6183                    ))),
6184                }
6185            }
6186            #[cfg(not(feature = "gpu"))]
6187            BuiltinId::ToCpu => Err(runtime_err(
6188                "GPU operations not available. Build with --features gpu",
6189            )),
6190
6191            #[cfg(feature = "gpu")]
6192            BuiltinId::GpuMatmul => {
6193                if args.len() < 2 {
6194                    return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6195                }
6196                let a = self.ensure_gpu_tensor(&args[0])?;
6197                let b = self.ensure_gpu_tensor(&args[1])?;
6198                let ops = self.get_gpu_ops()?;
6199                let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6200                Ok(VmValue::GpuTensor(Arc::new(result)))
6201            }
6202            #[cfg(not(feature = "gpu"))]
6203            BuiltinId::GpuMatmul => Err(runtime_err(
6204                "GPU operations not available. Build with --features gpu",
6205            )),
6206
6207            #[cfg(feature = "gpu")]
6208            BuiltinId::GpuBatchPredict => {
6209                if args.len() < 2 {
6210                    return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6211                }
6212                match (&args[0], &args[1]) {
6213                    (VmValue::Model(model), VmValue::Tensor(input)) => {
6214                        let batch_size = args.get(2).and_then(|v| match v {
6215                            VmValue::Int(n) => Some(*n as usize),
6216                            _ => None,
6217                        });
6218                        let result =
6219                            tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6220                                .map_err(runtime_err)?;
6221                        Ok(VmValue::Tensor(Arc::new(result)))
6222                    }
6223                    _ => Err(runtime_err(
6224                        "gpu_batch_predict() expects (model, tensor, [batch_size])",
6225                    )),
6226                }
6227            }
6228            #[cfg(not(feature = "gpu"))]
6229            BuiltinId::GpuBatchPredict => Err(runtime_err(
6230                "GPU operations not available. Build with --features gpu",
6231            )),
6232            // Phase 34: AI Agent Framework
6233            #[cfg(feature = "native")]
6234            BuiltinId::Embed => {
6235                if args.is_empty() {
6236                    return Err(runtime_err("embed() requires a text argument"));
6237                }
6238                let text = match &args[0] {
6239                    VmValue::String(s) => s.to_string(),
6240                    _ => return Err(runtime_err("embed() expects a string")),
6241                };
6242                let model = args
6243                    .get(1)
6244                    .and_then(|v| match v {
6245                        VmValue::String(s) => Some(s.to_string()),
6246                        _ => None,
6247                    })
6248                    .unwrap_or_else(|| "text-embedding-3-small".to_string());
6249                let api_key = args
6250                    .get(2)
6251                    .and_then(|v| match v {
6252                        VmValue::String(s) => Some(s.to_string()),
6253                        _ => None,
6254                    })
6255                    .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6256                    .ok_or_else(|| {
6257                        runtime_err(
6258                            "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6259                        )
6260                    })?;
6261                let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6262                    .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6263                Ok(VmValue::Tensor(Arc::new(tensor)))
6264            }
6265            #[cfg(not(feature = "native"))]
6266            BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6267            #[cfg(feature = "native")]
6268            BuiltinId::HttpRequest => {
6269                self.check_permission("network")?;
6270                if args.len() < 2 {
6271                    return Err(runtime_err(
6272                        "http_request(method, url, headers?, body?) expects at least 2 args",
6273                    ));
6274                }
6275                let method = match &args[0] {
6276                    VmValue::String(s) => s.to_string(),
6277                    _ => return Err(runtime_err("http_request() method must be a string")),
6278                };
6279                let url = match &args[1] {
6280                    VmValue::String(s) => s.to_string(),
6281                    _ => return Err(runtime_err("http_request() url must be a string")),
6282                };
6283                let client = reqwest::blocking::Client::new();
6284                let mut builder = match method.to_uppercase().as_str() {
6285                    "GET" => client.get(&url),
6286                    "POST" => client.post(&url),
6287                    "PUT" => client.put(&url),
6288                    "DELETE" => client.delete(&url),
6289                    "PATCH" => client.patch(&url),
6290                    "HEAD" => client.head(&url),
6291                    _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6292                };
6293                // Set headers if provided
6294                if let Some(VmValue::Map(headers)) = args.get(2) {
6295                    for (key, val) in headers.iter() {
6296                        if let VmValue::String(v) = val {
6297                            builder = builder.header(key.as_ref(), v.as_ref());
6298                        }
6299                    }
6300                }
6301                // Set body if provided
6302                if let Some(VmValue::String(body)) = args.get(3) {
6303                    builder = builder.body(body.as_ref().to_string());
6304                }
6305                let resp = builder
6306                    .send()
6307                    .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6308                let status = resp.status().as_u16() as i64;
6309                let body = resp
6310                    .text()
6311                    .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6312                Ok(VmValue::Map(Box::new(vec![
6313                    (Arc::from("status"), VmValue::Int(status)),
6314                    (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6315                ])))
6316            }
6317            #[cfg(not(feature = "native"))]
6318            BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6319            #[cfg(feature = "native")]
6320            BuiltinId::RunAgent => {
6321                self.check_permission("network")?;
6322                if args.len() < 2 {
6323                    return Err(runtime_err(
6324                        "run_agent(agent, message, [history]) expects at least 2 arguments",
6325                    ));
6326                }
6327                let agent_def = match &args[0] {
6328                    VmValue::AgentDef(def) => def.clone(),
6329                    _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6330                };
6331                let message = match &args[1] {
6332                    VmValue::String(s) => s.to_string(),
6333                    _ => return Err(runtime_err("run_agent() second arg must be a string")),
6334                };
6335                // Optional 3rd arg: conversation history as list of [role, content] pairs
6336                let history = if args.len() >= 3 {
6337                    match &args[2] {
6338                        VmValue::List(items) => {
6339                            let mut hist = Vec::new();
6340                            for item in items.iter() {
6341                                if let VmValue::List(pair) = item
6342                                    && pair.len() >= 2
6343                                {
6344                                    let role = match &pair[0] {
6345                                        VmValue::String(s) => s.to_string(),
6346                                        _ => continue,
6347                                    };
6348                                    let content = match &pair[1] {
6349                                        VmValue::String(s) => s.to_string(),
6350                                        _ => continue,
6351                                    };
6352                                    hist.push((role, content));
6353                                }
6354                            }
6355                            Some(hist)
6356                        }
6357                        _ => None,
6358                    }
6359                } else {
6360                    None
6361                };
6362                self.exec_agent_loop(&agent_def, &message, history.as_deref())
6363            }
6364            #[cfg(not(feature = "native"))]
6365            BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6366
6367            // Phase G4: Streaming agent responses
6368            #[cfg(feature = "native")]
6369            BuiltinId::StreamAgent => {
6370                self.check_permission("network")?;
6371                if args.len() < 3 {
6372                    return Err(runtime_err(
6373                        "stream_agent(agent, message, callback) expects 3 arguments",
6374                    ));
6375                }
6376                let agent_def = match &args[0] {
6377                    VmValue::AgentDef(def) => def.clone(),
6378                    _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6379                };
6380                let message = match &args[1] {
6381                    VmValue::String(s) => s.to_string(),
6382                    _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6383                };
6384                let callback = args[2].clone();
6385
6386                let model = &agent_def.model;
6387                let system = agent_def.system_prompt.as_deref();
6388                let base_url = agent_def.base_url.as_deref();
6389                let api_key = agent_def.api_key.as_deref();
6390
6391                let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6392                let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6393                    .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6394
6395                let mut full_text = String::new();
6396                loop {
6397                    match reader.next_chunk() {
6398                        Ok(Some(chunk)) => {
6399                            full_text.push_str(&chunk);
6400                            let chunk_val = VmValue::String(Arc::from(&*chunk));
6401                            let _ = self.call_value(callback.clone(), &[chunk_val]);
6402                        }
6403                        Ok(None) => break,
6404                        Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6405                    }
6406                }
6407
6408                Ok(VmValue::String(Arc::from(&*full_text)))
6409            }
6410            #[cfg(not(feature = "native"))]
6411            BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6412
6413            // Phase E5: Random & Sampling
6414            #[cfg(feature = "native")]
6415            BuiltinId::Random => {
6416                let mut rng = rand::thread_rng();
6417                let val: f64 = rand::Rng::r#gen(&mut rng);
6418                Ok(VmValue::Float(val))
6419            }
6420            #[cfg(not(feature = "native"))]
6421            BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6422            #[cfg(feature = "native")]
6423            BuiltinId::RandomInt => {
6424                if args.len() < 2 {
6425                    return Err(runtime_err("random_int() expects min and max"));
6426                }
6427                let a = match &args[0] {
6428                    VmValue::Int(n) => *n,
6429                    _ => return Err(runtime_err("random_int() expects integers")),
6430                };
6431                let b = match &args[1] {
6432                    VmValue::Int(n) => *n,
6433                    _ => return Err(runtime_err("random_int() expects integers")),
6434                };
6435                if a >= b {
6436                    return Err(runtime_err("random_int() requires min < max"));
6437                }
6438                let mut rng = rand::thread_rng();
6439                let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6440                Ok(VmValue::Int(val))
6441            }
6442            #[cfg(not(feature = "native"))]
6443            BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6444            #[cfg(feature = "native")]
6445            BuiltinId::Sample => {
6446                use rand::seq::SliceRandom;
6447                if args.is_empty() {
6448                    return Err(runtime_err("sample() expects a list and count"));
6449                }
6450                let items = match &args[0] {
6451                    VmValue::List(items) => items,
6452                    _ => return Err(runtime_err("sample() expects a list")),
6453                };
6454                let k = match args.get(1) {
6455                    Some(VmValue::Int(n)) => *n as usize,
6456                    _ => 1,
6457                };
6458                if k > items.len() {
6459                    return Err(runtime_err("sample() count exceeds list length"));
6460                }
6461                let mut rng = rand::thread_rng();
6462                let mut indices: Vec<usize> = (0..items.len()).collect();
6463                indices.partial_shuffle(&mut rng, k);
6464                let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6465                if k == 1 && args.get(1).is_none() {
6466                    Ok(result.into_iter().next().unwrap_or(VmValue::None))
6467                } else {
6468                    Ok(VmValue::List(Box::new(result)))
6469                }
6470            }
6471            #[cfg(not(feature = "native"))]
6472            BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6473
6474            // Phase E6: Math builtins
6475            BuiltinId::Exp => {
6476                let x = match args.first() {
6477                    Some(VmValue::Float(f)) => *f,
6478                    Some(VmValue::Int(n)) => *n as f64,
6479                    _ => return Err(runtime_err("exp() expects a number")),
6480                };
6481                Ok(VmValue::Float(x.exp()))
6482            }
6483            BuiltinId::IsNan => {
6484                let result = match args.first() {
6485                    Some(VmValue::Float(f)) => f.is_nan(),
6486                    _ => false,
6487                };
6488                Ok(VmValue::Bool(result))
6489            }
6490            BuiltinId::IsInfinite => {
6491                let result = match args.first() {
6492                    Some(VmValue::Float(f)) => f.is_infinite(),
6493                    _ => false,
6494                };
6495                Ok(VmValue::Bool(result))
6496            }
6497            BuiltinId::Sign => match args.first() {
6498                Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6499                    1
6500                } else if *n < 0 {
6501                    -1
6502                } else {
6503                    0
6504                })),
6505                Some(VmValue::Float(f)) => {
6506                    if f.is_nan() {
6507                        Ok(VmValue::Float(f64::NAN))
6508                    } else if *f > 0.0 {
6509                        Ok(VmValue::Int(1))
6510                    } else if *f < 0.0 {
6511                        Ok(VmValue::Int(-1))
6512                    } else {
6513                        Ok(VmValue::Int(0))
6514                    }
6515                }
6516                _ => Err(runtime_err("sign() expects a number")),
6517            },
6518            // Phase E8: Table assertion
6519            #[cfg(feature = "native")]
6520            BuiltinId::AssertTableEq => {
6521                if args.len() < 2 {
6522                    return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6523                }
6524                let t1 = match &args[0] {
6525                    VmValue::Table(t) => t,
6526                    _ => {
6527                        return Err(runtime_err(
6528                            "assert_table_eq() first argument must be a table",
6529                        ));
6530                    }
6531                };
6532                let t2 = match &args[1] {
6533                    VmValue::Table(t) => t,
6534                    _ => {
6535                        return Err(runtime_err(
6536                            "assert_table_eq() second argument must be a table",
6537                        ));
6538                    }
6539                };
6540                // Compare schemas
6541                if t1.df.schema() != t2.df.schema() {
6542                    return Err(runtime_err(format!(
6543                        "assert_table_eq: schemas differ\n  left:  {:?}\n  right: {:?}",
6544                        t1.df.schema(),
6545                        t2.df.schema()
6546                    )));
6547                }
6548                // Collect both DataFrames
6549                let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6550                let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6551                // Flatten into rows and compare
6552                let rows1: Vec<String> = batches1
6553                    .iter()
6554                    .flat_map(|b| {
6555                        (0..b.num_rows()).map(move |r| {
6556                            (0..b.num_columns())
6557                                .map(|c| {
6558                                    let col = b.column(c);
6559                                    format!("{:?}", col.slice(r, 1))
6560                                })
6561                                .collect::<Vec<_>>()
6562                                .join(",")
6563                        })
6564                    })
6565                    .collect();
6566                let rows2: Vec<String> = batches2
6567                    .iter()
6568                    .flat_map(|b| {
6569                        (0..b.num_rows()).map(move |r| {
6570                            (0..b.num_columns())
6571                                .map(|c| {
6572                                    let col = b.column(c);
6573                                    format!("{:?}", col.slice(r, 1))
6574                                })
6575                                .collect::<Vec<_>>()
6576                                .join(",")
6577                        })
6578                    })
6579                    .collect();
6580                if rows1.len() != rows2.len() {
6581                    return Err(runtime_err(format!(
6582                        "assert_table_eq: row count differs ({} vs {})",
6583                        rows1.len(),
6584                        rows2.len()
6585                    )));
6586                }
6587                for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
6588                    if r1 != r2 {
6589                        return Err(runtime_err(format!(
6590                            "assert_table_eq: row {} differs\n  left:  {}\n  right: {}",
6591                            i, r1, r2
6592                        )));
6593                    }
6594                }
6595                Ok(VmValue::None)
6596            }
6597            #[cfg(not(feature = "native"))]
6598            BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
6599
6600            // Phase F1: Date/Time builtins
6601            BuiltinId::Today => {
6602                use chrono::{Datelike, TimeZone};
6603                let now = chrono::Utc::now();
6604                let midnight = chrono::Utc
6605                    .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
6606                    .single()
6607                    .ok_or_else(|| runtime_err("Failed to compute today"))?;
6608                Ok(VmValue::DateTime(midnight.timestamp_millis()))
6609            }
6610            BuiltinId::DateAdd => {
6611                if args.len() < 3 {
6612                    return Err(runtime_err("date_add() expects datetime, amount, unit"));
6613                }
6614                let ms = match &args[0] {
6615                    VmValue::DateTime(ms) => *ms,
6616                    VmValue::Int(ms) => *ms,
6617                    _ => return Err(runtime_err("date_add() first arg must be datetime")),
6618                };
6619                let amount = match &args[1] {
6620                    VmValue::Int(n) => *n,
6621                    _ => return Err(runtime_err("date_add() amount must be an integer")),
6622                };
6623                let unit = match &args[2] {
6624                    VmValue::String(s) => s.as_ref(),
6625                    _ => return Err(runtime_err("date_add() unit must be a string")),
6626                };
6627                let offset_ms = match unit {
6628                    "second" | "seconds" => amount * 1000,
6629                    "minute" | "minutes" => amount * 60 * 1000,
6630                    "hour" | "hours" => amount * 3600 * 1000,
6631                    "day" | "days" => amount * 86400 * 1000,
6632                    "week" | "weeks" => amount * 7 * 86400 * 1000,
6633                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6634                };
6635                Ok(VmValue::DateTime(ms + offset_ms))
6636            }
6637            BuiltinId::DateDiff => {
6638                if args.len() < 3 {
6639                    return Err(runtime_err(
6640                        "date_diff() expects datetime1, datetime2, unit",
6641                    ));
6642                }
6643                let ms1 = match &args[0] {
6644                    VmValue::DateTime(ms) => *ms,
6645                    VmValue::Int(ms) => *ms,
6646                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
6647                };
6648                let ms2 = match &args[1] {
6649                    VmValue::DateTime(ms) => *ms,
6650                    VmValue::Int(ms) => *ms,
6651                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
6652                };
6653                let unit = match &args[2] {
6654                    VmValue::String(s) => s.as_ref(),
6655                    _ => return Err(runtime_err("date_diff() unit must be a string")),
6656                };
6657                let diff_ms = ms1 - ms2;
6658                let result = match unit {
6659                    "second" | "seconds" => diff_ms / 1000,
6660                    "minute" | "minutes" => diff_ms / (60 * 1000),
6661                    "hour" | "hours" => diff_ms / (3600 * 1000),
6662                    "day" | "days" => diff_ms / (86400 * 1000),
6663                    "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
6664                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6665                };
6666                Ok(VmValue::Int(result))
6667            }
6668            BuiltinId::DateTrunc => {
6669                if args.len() < 2 {
6670                    return Err(runtime_err("date_trunc() expects datetime and unit"));
6671                }
6672                let ms = match &args[0] {
6673                    VmValue::DateTime(ms) => *ms,
6674                    VmValue::Int(ms) => *ms,
6675                    _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
6676                };
6677                let unit = match &args[1] {
6678                    VmValue::String(s) => s.as_ref(),
6679                    _ => return Err(runtime_err("date_trunc() unit must be a string")),
6680                };
6681                use chrono::{Datelike, TimeZone, Timelike};
6682                let secs = ms / 1000;
6683                let dt = chrono::Utc
6684                    .timestamp_opt(secs, 0)
6685                    .single()
6686                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6687                let truncated = match unit {
6688                    "second" => chrono::Utc
6689                        .with_ymd_and_hms(
6690                            dt.year(),
6691                            dt.month(),
6692                            dt.day(),
6693                            dt.hour(),
6694                            dt.minute(),
6695                            dt.second(),
6696                        )
6697                        .single(),
6698                    "minute" => chrono::Utc
6699                        .with_ymd_and_hms(
6700                            dt.year(),
6701                            dt.month(),
6702                            dt.day(),
6703                            dt.hour(),
6704                            dt.minute(),
6705                            0,
6706                        )
6707                        .single(),
6708                    "hour" => chrono::Utc
6709                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
6710                        .single(),
6711                    "day" => chrono::Utc
6712                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
6713                        .single(),
6714                    "month" => chrono::Utc
6715                        .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
6716                        .single(),
6717                    "year" => chrono::Utc
6718                        .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
6719                        .single(),
6720                    _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
6721                };
6722                Ok(VmValue::DateTime(
6723                    truncated
6724                        .ok_or_else(|| runtime_err("Invalid truncation"))?
6725                        .timestamp_millis(),
6726                ))
6727            }
6728            BuiltinId::DateExtract => {
6729                if args.len() < 2 {
6730                    return Err(runtime_err("extract() expects datetime and part"));
6731                }
6732                let ms = match &args[0] {
6733                    VmValue::DateTime(ms) => *ms,
6734                    VmValue::Int(ms) => *ms,
6735                    _ => return Err(runtime_err("extract() first arg must be datetime")),
6736                };
6737                let part = match &args[1] {
6738                    VmValue::String(s) => s.as_ref(),
6739                    _ => return Err(runtime_err("extract() part must be a string")),
6740                };
6741                use chrono::{Datelike, TimeZone, Timelike};
6742                let secs = ms / 1000;
6743                let dt = chrono::Utc
6744                    .timestamp_opt(secs, 0)
6745                    .single()
6746                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6747                let val = match part {
6748                    "year" => dt.year() as i64,
6749                    "month" => dt.month() as i64,
6750                    "day" => dt.day() as i64,
6751                    "hour" => dt.hour() as i64,
6752                    "minute" => dt.minute() as i64,
6753                    "second" => dt.second() as i64,
6754                    "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
6755                    "day_of_year" | "doy" => dt.ordinal() as i64,
6756                    _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
6757                };
6758                Ok(VmValue::Int(val))
6759            }
6760
6761            // ── MCP builtins ──
6762            #[cfg(feature = "mcp")]
6763            BuiltinId::McpConnect => {
6764                if args.is_empty() {
6765                    return Err(runtime_err(
6766                        "mcp_connect expects at least 1 argument: command or URL",
6767                    ));
6768                }
6769                let command = match &args[0] {
6770                    VmValue::String(s) => s.to_string(),
6771                    _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
6772                };
6773
6774                // Build sampling callback when tl-ai is available (native feature)
6775                #[cfg(feature = "native")]
6776                let sampling_cb: Option<tl_mcp::SamplingCallback> =
6777                    Some(Arc::new(|req: tl_mcp::SamplingRequest| {
6778                        let model = req
6779                            .model_hint
6780                            .as_deref()
6781                            .unwrap_or("claude-sonnet-4-20250514");
6782                        let messages: Vec<serde_json::Value> = req
6783                            .messages
6784                            .iter()
6785                            .map(|(role, content)| {
6786                                serde_json::json!({"role": role, "content": content})
6787                            })
6788                            .collect();
6789                        let response = tl_ai::chat_with_tools(
6790                            model,
6791                            req.system_prompt.as_deref(),
6792                            &messages,
6793                            &[],  // no tools for sampling
6794                            None, // base_url
6795                            None, // api_key
6796                            None, // output_format
6797                        )
6798                        .map_err(|e| format!("Sampling LLM error: {e}"))?;
6799                        match response {
6800                            tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
6801                                model: model.to_string(),
6802                                content: text,
6803                                stop_reason: Some("endTurn".to_string()),
6804                            }),
6805                            tl_ai::LlmResponse::ToolUse(_) => {
6806                                Err("Sampling does not support tool use".to_string())
6807                            }
6808                        }
6809                    }));
6810
6811                #[cfg(not(feature = "native"))]
6812                let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
6813
6814                // Auto-detect HTTP URL vs subprocess command
6815                let client = if command.starts_with("http://") || command.starts_with("https://") {
6816                    tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
6817                        .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
6818                } else {
6819                    let cmd_args: Vec<String> = args[1..]
6820                        .iter()
6821                        .map(|a| match a {
6822                            VmValue::String(s) => s.to_string(),
6823                            other => format!("{}", other),
6824                        })
6825                        .collect();
6826                    tl_mcp::McpClient::connect_with_sampling(
6827                        &command,
6828                        &cmd_args,
6829                        self.security_policy.as_ref(),
6830                        sampling_cb,
6831                    )
6832                    .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
6833                };
6834                Ok(VmValue::McpClient(Arc::new(client)))
6835            }
6836            #[cfg(not(feature = "mcp"))]
6837            BuiltinId::McpConnect => {
6838                Err(runtime_err("MCP not available. Build with --features mcp"))
6839            }
6840
6841            #[cfg(feature = "mcp")]
6842            BuiltinId::McpListTools => {
6843                if args.is_empty() {
6844                    return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
6845                }
6846                match &args[0] {
6847                    VmValue::McpClient(client) => {
6848                        let tools = client
6849                            .list_tools()
6850                            .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
6851                        let tool_values: Vec<VmValue> = tools
6852                            .iter()
6853                            .map(|tool| {
6854                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6855                                pairs.push((
6856                                    Arc::from("name"),
6857                                    VmValue::String(Arc::from(tool.name.as_ref())),
6858                                ));
6859                                if let Some(desc) = &tool.description {
6860                                    pairs.push((
6861                                        Arc::from("description"),
6862                                        VmValue::String(Arc::from(desc.as_ref())),
6863                                    ));
6864                                }
6865                                let schema_json = serde_json::to_string(tool.input_schema.as_ref())
6866                                    .unwrap_or_default();
6867                                if !schema_json.is_empty() && schema_json != "{}" {
6868                                    pairs.push((
6869                                        Arc::from("input_schema"),
6870                                        VmValue::String(Arc::from(schema_json.as_str())),
6871                                    ));
6872                                }
6873                                VmValue::Map(Box::new(pairs))
6874                            })
6875                            .collect();
6876                        Ok(VmValue::List(Box::new(tool_values)))
6877                    }
6878                    _ => Err(runtime_err(
6879                        "mcp_list_tools: argument must be an mcp_client",
6880                    )),
6881                }
6882            }
6883            #[cfg(not(feature = "mcp"))]
6884            BuiltinId::McpListTools => {
6885                Err(runtime_err("MCP not available. Build with --features mcp"))
6886            }
6887
6888            #[cfg(feature = "mcp")]
6889            BuiltinId::McpCallTool => {
6890                if args.len() < 2 {
6891                    return Err(runtime_err(
6892                        "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
6893                    ));
6894                }
6895                let client = match &args[0] {
6896                    VmValue::McpClient(c) => c.clone(),
6897                    _ => {
6898                        return Err(runtime_err(
6899                            "mcp_call_tool: first argument must be an mcp_client",
6900                        ));
6901                    }
6902                };
6903                let tool_name = match &args[1] {
6904                    VmValue::String(s) => s.to_string(),
6905                    _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
6906                };
6907                let arguments = if args.len() > 2 {
6908                    vm_value_to_json(&args[2])
6909                } else {
6910                    serde_json::Value::Object(serde_json::Map::new())
6911                };
6912                let result = client
6913                    .call_tool(&tool_name, arguments)
6914                    .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
6915                let mut content_parts: Vec<VmValue> = Vec::new();
6916                for content in &result.content {
6917                    if let Some(text) = content.as_text() {
6918                        content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
6919                    }
6920                }
6921                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6922                if content_parts.len() == 1 {
6923                    pairs.push((
6924                        Arc::from("content"),
6925                        content_parts.into_iter().next().unwrap(),
6926                    ));
6927                } else {
6928                    pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
6929                }
6930                pairs.push((
6931                    Arc::from("is_error"),
6932                    VmValue::Bool(result.is_error.unwrap_or(false)),
6933                ));
6934                Ok(VmValue::Map(Box::new(pairs)))
6935            }
6936            #[cfg(not(feature = "mcp"))]
6937            BuiltinId::McpCallTool => {
6938                Err(runtime_err("MCP not available. Build with --features mcp"))
6939            }
6940
6941            #[cfg(feature = "mcp")]
6942            BuiltinId::McpDisconnect => {
6943                if args.is_empty() {
6944                    return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
6945                }
6946                match &args[0] {
6947                    VmValue::McpClient(_) => Ok(VmValue::None),
6948                    _ => Err(runtime_err(
6949                        "mcp_disconnect: argument must be an mcp_client",
6950                    )),
6951                }
6952            }
6953            #[cfg(not(feature = "mcp"))]
6954            BuiltinId::McpDisconnect => {
6955                Err(runtime_err("MCP not available. Build with --features mcp"))
6956            }
6957
6958            #[cfg(feature = "mcp")]
6959            BuiltinId::McpPing => {
6960                if args.is_empty() {
6961                    return Err(runtime_err("mcp_ping expects 1 argument: client"));
6962                }
6963                match &args[0] {
6964                    VmValue::McpClient(client) => {
6965                        client
6966                            .ping()
6967                            .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
6968                        Ok(VmValue::Bool(true))
6969                    }
6970                    _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
6971                }
6972            }
6973            #[cfg(not(feature = "mcp"))]
6974            BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
6975
6976            #[cfg(feature = "mcp")]
6977            BuiltinId::McpServerInfo => {
6978                if args.is_empty() {
6979                    return Err(runtime_err("mcp_server_info expects 1 argument: client"));
6980                }
6981                match &args[0] {
6982                    VmValue::McpClient(client) => match client.server_info() {
6983                        Some(info) => {
6984                            let pairs: Vec<(Arc<str>, VmValue)> = vec![
6985                                (
6986                                    Arc::from("name"),
6987                                    VmValue::String(Arc::from(info.server_info.name.as_str())),
6988                                ),
6989                                (
6990                                    Arc::from("version"),
6991                                    VmValue::String(Arc::from(info.server_info.version.as_str())),
6992                                ),
6993                            ];
6994                            Ok(VmValue::Map(Box::new(pairs)))
6995                        }
6996                        None => Ok(VmValue::None),
6997                    },
6998                    _ => Err(runtime_err(
6999                        "mcp_server_info: argument must be an mcp_client",
7000                    )),
7001                }
7002            }
7003            #[cfg(not(feature = "mcp"))]
7004            BuiltinId::McpServerInfo => {
7005                Err(runtime_err("MCP not available. Build with --features mcp"))
7006            }
7007
7008            #[cfg(feature = "mcp")]
7009            BuiltinId::McpServe => {
7010                self.check_permission("network")?;
7011                if args.is_empty() {
7012                    return Err(runtime_err(
7013                        "mcp_serve expects 1 argument: list of tool definitions",
7014                    ));
7015                }
7016                let tool_list = match &args[0] {
7017                    VmValue::List(items) => items.as_ref().clone(),
7018                    _ => {
7019                        return Err(runtime_err(
7020                            "mcp_serve: argument must be a list of tool maps",
7021                        ));
7022                    }
7023                };
7024
7025                // Extract tool definitions and function values
7026                let mut channel_tools = Vec::new();
7027                let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
7028
7029                for item in &tool_list {
7030                    let pairs = match item {
7031                        VmValue::Map(p) => p.as_ref(),
7032                        _ => {
7033                            return Err(runtime_err(
7034                                "mcp_serve: each tool must be a map with name, description, handler",
7035                            ));
7036                        }
7037                    };
7038                    let mut name = String::new();
7039                    let mut description = String::new();
7040                    let mut handler = None;
7041                    let mut input_schema = serde_json::json!({"type": "object"});
7042
7043                    for (k, v) in pairs {
7044                        match k.as_ref() {
7045                            "name" => {
7046                                if let VmValue::String(s) = v {
7047                                    name = s.to_string();
7048                                }
7049                            }
7050                            "description" => {
7051                                if let VmValue::String(s) = v {
7052                                    description = s.to_string();
7053                                }
7054                            }
7055                            "handler" => {
7056                                handler = Some(v.clone());
7057                            }
7058                            "input_schema" | "parameters" => {
7059                                if let VmValue::String(s) = v
7060                                    && let Ok(parsed) =
7061                                        serde_json::from_str::<serde_json::Value>(s.as_ref())
7062                                {
7063                                    input_schema = parsed;
7064                                }
7065                            }
7066                            _ => {}
7067                        }
7068                    }
7069
7070                    if name.is_empty() {
7071                        return Err(runtime_err("mcp_serve: tool missing 'name'"));
7072                    }
7073                    if let Some(h) = handler {
7074                        tool_handlers.insert(name.clone(), h);
7075                    }
7076
7077                    channel_tools.push(tl_mcp::server::ChannelToolDef {
7078                        name,
7079                        description,
7080                        input_schema,
7081                    });
7082                }
7083
7084                // Build server with channel-based tools
7085                let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7086                    .name("tl-mcp-server")
7087                    .version("1.0.0")
7088                    .channel_tools(channel_tools);
7089                let server_handler = builder.build();
7090
7091                // Start server on background thread
7092                let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7093
7094                // Main dispatch loop: process tool call requests from the MCP server
7095                while let Ok(req) = rx.recv() {
7096                    let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7097                        // Convert JSON args to VmValue args
7098                        let call_args = self.json_to_vm_args(&req.arguments);
7099                        match self.call_value(func.clone(), &call_args) {
7100                            Ok(val) => {
7101                                // Convert VmValue to JSON-friendly string
7102                                Ok(serde_json::json!(format!("{val}")))
7103                            }
7104                            Err(e) => Err(format!("{e}")),
7105                        }
7106                    } else {
7107                        Err(format!("Unknown tool: {}", req.tool_name))
7108                    };
7109                    let _ = req.response_tx.send(result);
7110                }
7111
7112                Ok(VmValue::None)
7113            }
7114            #[cfg(not(feature = "mcp"))]
7115            BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7116
7117            // ── MCP Resources & Prompts ──
7118            #[cfg(feature = "mcp")]
7119            BuiltinId::McpListResources => {
7120                if args.is_empty() {
7121                    return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7122                }
7123                match &args[0] {
7124                    VmValue::McpClient(client) => {
7125                        let resources = client
7126                            .list_resources()
7127                            .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7128                        let vals: Vec<VmValue> = resources
7129                            .iter()
7130                            .map(|r| {
7131                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7132                                pairs.push((
7133                                    Arc::from("uri"),
7134                                    VmValue::String(Arc::from(r.uri.as_str())),
7135                                ));
7136                                pairs.push((
7137                                    Arc::from("name"),
7138                                    VmValue::String(Arc::from(r.name.as_str())),
7139                                ));
7140                                if let Some(desc) = &r.description {
7141                                    pairs.push((
7142                                        Arc::from("description"),
7143                                        VmValue::String(Arc::from(desc.as_str())),
7144                                    ));
7145                                }
7146                                if let Some(mime) = &r.mime_type {
7147                                    pairs.push((
7148                                        Arc::from("mime_type"),
7149                                        VmValue::String(Arc::from(mime.as_str())),
7150                                    ));
7151                                }
7152                                VmValue::Map(Box::new(pairs))
7153                            })
7154                            .collect();
7155                        Ok(VmValue::List(Box::new(vals)))
7156                    }
7157                    _ => Err(runtime_err(
7158                        "mcp_list_resources: argument must be an mcp_client",
7159                    )),
7160                }
7161            }
7162            #[cfg(not(feature = "mcp"))]
7163            BuiltinId::McpListResources => {
7164                Err(runtime_err("MCP not available. Build with --features mcp"))
7165            }
7166
7167            #[cfg(feature = "mcp")]
7168            BuiltinId::McpReadResource => {
7169                if args.len() < 2 {
7170                    return Err(runtime_err(
7171                        "mcp_read_resource expects 2 arguments: client, uri",
7172                    ));
7173                }
7174                let client = match &args[0] {
7175                    VmValue::McpClient(c) => c.clone(),
7176                    _ => {
7177                        return Err(runtime_err(
7178                            "mcp_read_resource: first argument must be an mcp_client",
7179                        ));
7180                    }
7181                };
7182                let uri = match &args[1] {
7183                    VmValue::String(s) => s.to_string(),
7184                    _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7185                };
7186                let result = client
7187                    .read_resource(&uri)
7188                    .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7189                // Serialize ResourceContents via JSON to avoid direct rmcp type dependency
7190                let contents: Vec<VmValue> = result
7191                    .contents
7192                    .iter()
7193                    .map(|content| {
7194                        let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7195                        let json = serde_json::to_value(content).unwrap_or_default();
7196                        if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7197                            pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7198                        }
7199                        if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7200                            pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7201                        }
7202                        if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7203                            pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7204                        }
7205                        if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7206                            pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7207                        }
7208                        VmValue::Map(Box::new(pairs))
7209                    })
7210                    .collect();
7211                if contents.len() == 1 {
7212                    Ok(contents.into_iter().next().unwrap())
7213                } else {
7214                    Ok(VmValue::List(Box::new(contents)))
7215                }
7216            }
7217            #[cfg(not(feature = "mcp"))]
7218            BuiltinId::McpReadResource => {
7219                Err(runtime_err("MCP not available. Build with --features mcp"))
7220            }
7221
7222            #[cfg(feature = "mcp")]
7223            BuiltinId::McpListPrompts => {
7224                if args.is_empty() {
7225                    return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7226                }
7227                match &args[0] {
7228                    VmValue::McpClient(client) => {
7229                        let prompts = client
7230                            .list_prompts()
7231                            .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7232                        let vals: Vec<VmValue> = prompts
7233                            .iter()
7234                            .map(|p| {
7235                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7236                                pairs.push((
7237                                    Arc::from("name"),
7238                                    VmValue::String(Arc::from(p.name.as_str())),
7239                                ));
7240                                if let Some(desc) = &p.description {
7241                                    pairs.push((
7242                                        Arc::from("description"),
7243                                        VmValue::String(Arc::from(desc.as_str())),
7244                                    ));
7245                                }
7246                                if let Some(prompt_args) = &p.arguments {
7247                                    let arg_vals: Vec<VmValue> = prompt_args
7248                                        .iter()
7249                                        .map(|a| {
7250                                            let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7251                                                Vec::new();
7252                                            arg_pairs.push((
7253                                                Arc::from("name"),
7254                                                VmValue::String(Arc::from(a.name.as_str())),
7255                                            ));
7256                                            if let Some(desc) = &a.description {
7257                                                arg_pairs.push((
7258                                                    Arc::from("description"),
7259                                                    VmValue::String(Arc::from(desc.as_str())),
7260                                                ));
7261                                            }
7262                                            arg_pairs.push((
7263                                                Arc::from("required"),
7264                                                VmValue::Bool(a.required.unwrap_or(false)),
7265                                            ));
7266                                            VmValue::Map(Box::new(arg_pairs))
7267                                        })
7268                                        .collect();
7269                                    pairs.push((
7270                                        Arc::from("arguments"),
7271                                        VmValue::List(Box::new(arg_vals)),
7272                                    ));
7273                                }
7274                                VmValue::Map(Box::new(pairs))
7275                            })
7276                            .collect();
7277                        Ok(VmValue::List(Box::new(vals)))
7278                    }
7279                    _ => Err(runtime_err(
7280                        "mcp_list_prompts: argument must be an mcp_client",
7281                    )),
7282                }
7283            }
7284            #[cfg(not(feature = "mcp"))]
7285            BuiltinId::McpListPrompts => {
7286                Err(runtime_err("MCP not available. Build with --features mcp"))
7287            }
7288
7289            #[cfg(feature = "mcp")]
7290            BuiltinId::McpGetPrompt => {
7291                if args.len() < 2 {
7292                    return Err(runtime_err(
7293                        "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7294                    ));
7295                }
7296                let client = match &args[0] {
7297                    VmValue::McpClient(c) => c.clone(),
7298                    _ => {
7299                        return Err(runtime_err(
7300                            "mcp_get_prompt: first argument must be an mcp_client",
7301                        ));
7302                    }
7303                };
7304                let name = match &args[1] {
7305                    VmValue::String(s) => s.to_string(),
7306                    _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7307                };
7308                let prompt_args = if args.len() > 2 {
7309                    let json = vm_value_to_json(&args[2]);
7310                    json.as_object().cloned()
7311                } else {
7312                    None
7313                };
7314                let result = client
7315                    .get_prompt(&name, prompt_args)
7316                    .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7317                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7318                if let Some(desc) = &result.description {
7319                    pairs.push((
7320                        Arc::from("description"),
7321                        VmValue::String(Arc::from(desc.as_str())),
7322                    ));
7323                }
7324                // Serialize PromptMessage via JSON to avoid direct rmcp type dependency
7325                let messages: Vec<VmValue> = result
7326                    .messages
7327                    .iter()
7328                    .map(|m| {
7329                        let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7330                        let msg_json = serde_json::to_value(m).unwrap_or_default();
7331                        // role is serialized as "user" or "assistant"
7332                        if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7333                            msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7334                        }
7335                        // content is an object with "type" field; extract text if it's a text message
7336                        if let Some(content) = msg_json.get("content") {
7337                            if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7338                                msg_pairs
7339                                    .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7340                            } else {
7341                                let content_str = content.to_string();
7342                                msg_pairs.push((
7343                                    Arc::from("content"),
7344                                    VmValue::String(Arc::from(content_str.as_str())),
7345                                ));
7346                            }
7347                        }
7348                        VmValue::Map(Box::new(msg_pairs))
7349                    })
7350                    .collect();
7351                pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7352                Ok(VmValue::Map(Box::new(pairs)))
7353            }
7354            #[cfg(not(feature = "mcp"))]
7355            BuiltinId::McpGetPrompt => {
7356                Err(runtime_err("MCP not available. Build with --features mcp"))
7357            }
7358        }
7359    }
7360
7361    // ── AI helpers ──
7362
7363    fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7364        match val {
7365            VmValue::List(items) => items
7366                .iter()
7367                .map(|item| match item {
7368                    VmValue::Int(n) => Ok(*n as f64),
7369                    VmValue::Float(f) => Ok(*f),
7370                    _ => Err(runtime_err("Expected number in list")),
7371                })
7372                .collect(),
7373            VmValue::Int(n) => Ok(vec![*n as f64]),
7374            VmValue::Float(f) => Ok(vec![*f]),
7375            _ => Err(runtime_err("Expected a list of numbers")),
7376        }
7377    }
7378
7379    fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7380        match val {
7381            VmValue::List(items) => items
7382                .iter()
7383                .map(|item| match item {
7384                    VmValue::Int(n) => Ok(*n as usize),
7385                    _ => Err(runtime_err("Expected integer in shape list")),
7386                })
7387                .collect(),
7388            _ => Err(runtime_err("Expected a list of integers for shape")),
7389        }
7390    }
7391
7392    #[cfg(feature = "native")]
7393    fn handle_train(
7394        &mut self,
7395        frame_idx: usize,
7396        algo_const: u8,
7397        config_const: u8,
7398    ) -> Result<VmValue, TlError> {
7399        let frame = &self.frames[frame_idx];
7400        let algorithm = match &frame.prototype.constants[algo_const as usize] {
7401            Constant::String(s) => s.to_string(),
7402            _ => return Err(runtime_err("Expected string constant for algorithm")),
7403        };
7404        let config_args = match &frame.prototype.constants[config_const as usize] {
7405            Constant::AstExprList(args) => args.clone(),
7406            _ => return Err(runtime_err("Expected AST expr list for train config")),
7407        };
7408
7409        // Extract config values
7410        let mut data_val = None;
7411        let mut target_name = None;
7412        let mut feature_names: Vec<String> = Vec::new();
7413
7414        for arg in &config_args {
7415            if let AstExpr::NamedArg { name, value } = arg {
7416                match name.as_str() {
7417                    "data" => {
7418                        data_val = Some(self.eval_ast_to_vm(value)?);
7419                    }
7420                    "target" => {
7421                        if let AstExpr::String(s) = value.as_ref() {
7422                            target_name = Some(s.clone());
7423                        }
7424                    }
7425                    "features" => {
7426                        if let AstExpr::List(items) = value.as_ref() {
7427                            for item in items {
7428                                if let AstExpr::String(s) = item {
7429                                    feature_names.push(s.clone());
7430                                }
7431                            }
7432                        }
7433                    }
7434                    _ => {}
7435                }
7436            }
7437        }
7438
7439        // Build training config from table data
7440        let table = match data_val {
7441            Some(VmValue::Table(t)) => t,
7442            _ => return Err(runtime_err("train: data must be a table")),
7443        };
7444        let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7445
7446        // Collect table to Arrow batches
7447        let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7448        if batches.is_empty() {
7449            return Err(runtime_err("train: empty dataset"));
7450        }
7451
7452        // Determine feature columns if not specified
7453        let batch = &batches[0];
7454        let schema = batch.schema();
7455        if feature_names.is_empty() {
7456            for field in schema.fields() {
7457                if field.name() != &target {
7458                    feature_names.push(field.name().clone());
7459                }
7460            }
7461        }
7462
7463        // Extract feature data and target data as f64 arrays
7464        let n_rows = batch.num_rows();
7465        let n_features = feature_names.len();
7466        let mut features_data = Vec::with_capacity(n_rows * n_features);
7467        let mut target_data = Vec::with_capacity(n_rows);
7468
7469        for col_name in &feature_names {
7470            let col_idx = schema
7471                .index_of(col_name)
7472                .map_err(|_| runtime_err(format!("Column not found: {col_name}")))?;
7473            let col_arr = batch.column(col_idx);
7474            Self::extract_f64_column(col_arr, &mut features_data)?;
7475        }
7476
7477        // Extract target column
7478        let target_idx = schema
7479            .index_of(&target)
7480            .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7481        let target_arr = batch.column(target_idx);
7482        Self::extract_f64_column(target_arr, &mut target_data)?;
7483
7484        // Reshape features: [col1_row1, col1_row2, ..., col2_row1, ...] → row-major
7485        let mut row_major = Vec::with_capacity(n_rows * n_features);
7486        for row in 0..n_rows {
7487            for col in 0..n_features {
7488                row_major.push(features_data[col * n_rows + row]);
7489            }
7490        }
7491
7492        let features_tensor = tl_ai::TlTensor::from_vec(row_major, &[n_rows, n_features])
7493            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7494        let target_tensor = tl_ai::TlTensor::from_vec(target_data, &[n_rows])
7495            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7496
7497        let config = tl_ai::TrainConfig {
7498            features: features_tensor,
7499            target: target_tensor,
7500            feature_names: feature_names.clone(),
7501            target_name: target.clone(),
7502            model_name: algorithm.clone(),
7503            split_ratio: 0.8,
7504            hyperparams: std::collections::HashMap::new(),
7505        };
7506
7507        let model = tl_ai::train(&algorithm, &config)
7508            .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7509
7510        Ok(VmValue::Model(Arc::new(model)))
7511    }
7512
7513    #[cfg(feature = "native")]
7514    fn extract_f64_column(
7515        col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7516        out: &mut Vec<f64>,
7517    ) -> Result<(), TlError> {
7518        use tl_data::datafusion::arrow::array::{
7519            Array, Float32Array, Float64Array, Int32Array, Int64Array,
7520        };
7521        let len = col.len();
7522        if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
7523            for i in 0..len {
7524                out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
7525            }
7526        } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
7527            for i in 0..len {
7528                out.push(if arr.is_null(i) {
7529                    0.0
7530                } else {
7531                    arr.value(i) as f64
7532                });
7533            }
7534        } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
7535            for i in 0..len {
7536                out.push(if arr.is_null(i) {
7537                    0.0
7538                } else {
7539                    arr.value(i) as f64
7540                });
7541            }
7542        } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
7543            for i in 0..len {
7544                out.push(if arr.is_null(i) {
7545                    0.0
7546                } else {
7547                    arr.value(i) as f64
7548                });
7549            }
7550        } else {
7551            return Err(runtime_err(
7552                "Column must be numeric (int32, int64, float32, float64)",
7553            ));
7554        }
7555        Ok(())
7556    }
7557
7558    #[cfg(feature = "native")]
7559    fn handle_pipeline_exec(
7560        &mut self,
7561        frame_idx: usize,
7562        name_const: u8,
7563        config_const: u8,
7564    ) -> Result<VmValue, TlError> {
7565        let frame = &self.frames[frame_idx];
7566        let name = match &frame.prototype.constants[name_const as usize] {
7567            Constant::String(s) => s.to_string(),
7568            _ => return Err(runtime_err("Expected string constant for pipeline name")),
7569        };
7570
7571        let mut schedule = None;
7572        let mut timeout_ms = None;
7573        let mut retries = 0u32;
7574
7575        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7576            for arg in args {
7577                if let AstExpr::NamedArg { name: key, value } = arg {
7578                    match key.as_str() {
7579                        "schedule" => {
7580                            if let AstExpr::String(s) = value.as_ref() {
7581                                schedule = Some(s.clone());
7582                            }
7583                        }
7584                        "timeout" => {
7585                            if let AstExpr::String(s) = value.as_ref() {
7586                                timeout_ms = tl_stream::parse_duration(s).ok();
7587                            }
7588                        }
7589                        "retries" => {
7590                            if let AstExpr::Int(n) = value.as_ref() {
7591                                retries = *n as u32;
7592                            }
7593                        }
7594                        _ => {}
7595                    }
7596                }
7597            }
7598        }
7599
7600        let def = tl_stream::PipelineDef {
7601            name,
7602            schedule,
7603            timeout_ms,
7604            retries,
7605        };
7606
7607        self.output
7608            .push(format!("Pipeline '{}': success", def.name));
7609        Ok(VmValue::PipelineDef(Arc::new(def)))
7610    }
7611
7612    #[cfg(feature = "native")]
7613    fn handle_stream_exec(
7614        &mut self,
7615        frame_idx: usize,
7616        config_const: u8,
7617    ) -> Result<VmValue, TlError> {
7618        let frame = &self.frames[frame_idx];
7619        let config_args = match &frame.prototype.constants[config_const as usize] {
7620            Constant::AstExprList(args) => args.clone(),
7621            _ => return Err(runtime_err("Expected AST expr list for stream config")),
7622        };
7623
7624        let mut name = String::new();
7625        let mut window = None;
7626        let mut watermark_ms = None;
7627
7628        for arg in &config_args {
7629            if let AstExpr::NamedArg { name: key, value } = arg {
7630                match key.as_str() {
7631                    "name" => {
7632                        if let AstExpr::String(s) = value.as_ref() {
7633                            name = s.clone();
7634                        }
7635                    }
7636                    "window" => {
7637                        if let AstExpr::String(s) = value.as_ref() {
7638                            window = Self::parse_window_type(s);
7639                        }
7640                    }
7641                    "watermark" => {
7642                        if let AstExpr::String(s) = value.as_ref() {
7643                            watermark_ms = tl_stream::parse_duration(s).ok();
7644                        }
7645                    }
7646                    _ => {}
7647                }
7648            }
7649        }
7650
7651        let def = tl_stream::StreamDef {
7652            name: name.clone(),
7653            window,
7654            watermark_ms,
7655        };
7656
7657        self.output.push(format!("Stream '{}' declared", name));
7658        Ok(VmValue::StreamDef(Arc::new(def)))
7659    }
7660
7661    #[cfg(feature = "native")]
7662    fn handle_agent_exec(
7663        &mut self,
7664        frame_idx: usize,
7665        name_const: u8,
7666        config_const: u8,
7667    ) -> Result<VmValue, TlError> {
7668        let frame = &self.frames[frame_idx];
7669        let name = match &frame.prototype.constants[name_const as usize] {
7670            Constant::String(s) => s.to_string(),
7671            _ => return Err(runtime_err("Expected string constant for agent name")),
7672        };
7673
7674        let mut model = String::new();
7675        let mut system_prompt = None;
7676        let mut max_turns = 10u32;
7677        let mut temperature = None;
7678        let mut max_tokens = None;
7679        let mut base_url = None;
7680        let mut api_key = None;
7681        let mut output_format = None;
7682        let mut tools = Vec::new();
7683        #[cfg(feature = "mcp")]
7684        let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
7685
7686        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7687            for arg in args {
7688                if let AstExpr::NamedArg { name: key, value } = arg {
7689                    if let Some(tool_name) = key.strip_prefix("tool:") {
7690                        // Tool definition — extract description and parameters from map expr
7691                        let (desc, params) = Self::extract_tool_from_ast(value);
7692                        tools.push(tl_stream::AgentTool {
7693                            name: tool_name.to_string(),
7694                            description: desc,
7695                            parameters: params,
7696                        });
7697                    } else if key.starts_with("mcp_server:") {
7698                        // MCP server reference — look up variable in globals
7699                        #[cfg(feature = "mcp")]
7700                        if let AstExpr::Ident(var_name) = value.as_ref()
7701                            && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
7702                        {
7703                            mcp_clients.push(client.clone());
7704                        }
7705                    } else {
7706                        match key.as_str() {
7707                            "model" => {
7708                                if let AstExpr::String(s) = value.as_ref() {
7709                                    model = s.clone();
7710                                }
7711                            }
7712                            "system" => {
7713                                if let AstExpr::String(s) = value.as_ref() {
7714                                    system_prompt = Some(s.clone());
7715                                }
7716                            }
7717                            "max_turns" => {
7718                                if let AstExpr::Int(n) = value.as_ref() {
7719                                    max_turns = *n as u32;
7720                                }
7721                            }
7722                            "temperature" => {
7723                                if let AstExpr::Float(f) = value.as_ref() {
7724                                    temperature = Some(*f);
7725                                }
7726                            }
7727                            "max_tokens" => {
7728                                if let AstExpr::Int(n) = value.as_ref() {
7729                                    max_tokens = Some(*n as u32);
7730                                }
7731                            }
7732                            "base_url" => {
7733                                if let AstExpr::String(s) = value.as_ref() {
7734                                    base_url = Some(s.clone());
7735                                }
7736                            }
7737                            "api_key" => {
7738                                if let AstExpr::String(s) = value.as_ref() {
7739                                    api_key = Some(s.clone());
7740                                }
7741                            }
7742                            "output_format" => {
7743                                if let AstExpr::String(s) = value.as_ref() {
7744                                    output_format = Some(s.clone());
7745                                }
7746                            }
7747                            _ => {}
7748                        }
7749                    }
7750                }
7751            }
7752        }
7753
7754        let def = tl_stream::AgentDef {
7755            name: name.clone(),
7756            model,
7757            system_prompt,
7758            tools,
7759            max_turns,
7760            temperature,
7761            max_tokens,
7762            base_url,
7763            api_key,
7764            output_format,
7765        };
7766
7767        // Store MCP clients for this agent
7768        #[cfg(feature = "mcp")]
7769        if !mcp_clients.is_empty() {
7770            self.mcp_agent_clients.insert(name.clone(), mcp_clients);
7771        }
7772
7773        Ok(VmValue::AgentDef(Arc::new(def)))
7774    }
7775
7776    #[cfg(feature = "native")]
7777    fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
7778        let mut desc = String::new();
7779        let mut params = serde_json::Value::Object(serde_json::Map::new());
7780        if let AstExpr::Map(pairs) = expr {
7781            for (key_expr, val_expr) in pairs {
7782                if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
7783                    match key.as_str() {
7784                        "description" => {
7785                            if let AstExpr::String(s) = val_expr {
7786                                desc = s.clone();
7787                            }
7788                        }
7789                        "parameters" => {
7790                            params = Self::ast_to_json(val_expr);
7791                        }
7792                        _ => {}
7793                    }
7794                }
7795            }
7796        }
7797        (desc, params)
7798    }
7799
7800    #[cfg(feature = "native")]
7801    fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
7802        match expr {
7803            AstExpr::String(s) => serde_json::Value::String(s.clone()),
7804            AstExpr::Int(n) => serde_json::json!(*n),
7805            AstExpr::Float(f) => serde_json::json!(*f),
7806            AstExpr::Bool(b) => serde_json::Value::Bool(*b),
7807            AstExpr::None => serde_json::Value::Null,
7808            AstExpr::List(items) => {
7809                serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
7810            }
7811            AstExpr::Map(pairs) => {
7812                let mut map = serde_json::Map::new();
7813                for (k, v) in pairs {
7814                    let key = match k {
7815                        AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
7816                        _ => format!("{k:?}"),
7817                    };
7818                    map.insert(key, Self::ast_to_json(v));
7819                }
7820                serde_json::Value::Object(map)
7821            }
7822            _ => serde_json::Value::Null,
7823        }
7824    }
7825
7826    #[cfg(feature = "native")]
7827    fn exec_agent_loop(
7828        &mut self,
7829        agent_def: &tl_stream::AgentDef,
7830        user_message: &str,
7831        history: Option<&[(String, String)]>,
7832    ) -> Result<VmValue, TlError> {
7833        use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
7834
7835        let model = &agent_def.model;
7836        let system = agent_def.system_prompt.as_deref();
7837        let base_url = agent_def.base_url.as_deref();
7838        let api_key = agent_def.api_key.as_deref();
7839
7840        let provider = if model.starts_with("claude") {
7841            "anthropic"
7842        } else {
7843            "openai"
7844        };
7845
7846        // Build tools JSON in OpenAI format from TL-declared tools
7847        #[allow(unused_mut)]
7848        let mut tools_json: Vec<serde_json::Value> = agent_def
7849            .tools
7850            .iter()
7851            .map(|t| {
7852                serde_json::json!({
7853                    "type": "function",
7854                    "function": {
7855                        "name": t.name,
7856                        "description": t.description,
7857                        "parameters": t.parameters
7858                    }
7859                })
7860            })
7861            .collect();
7862
7863        // Add MCP tools from connected servers
7864        #[cfg(feature = "mcp")]
7865        let mcp_clients = self
7866            .mcp_agent_clients
7867            .get(&agent_def.name)
7868            .cloned()
7869            .unwrap_or_default();
7870        #[cfg(feature = "mcp")]
7871        let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
7872            let mut dispatch = std::collections::HashMap::new();
7873            for (client_idx, client) in mcp_clients.iter().enumerate() {
7874                if let Ok(mcp_tools) = client.list_tools() {
7875                    for tool in mcp_tools {
7876                        let tool_name = tool.name.to_string();
7877                        tools_json.push(serde_json::json!({
7878                            "type": "function",
7879                            "function": {
7880                                "name": &tool_name,
7881                                "description": tool.description.as_deref().unwrap_or(""),
7882                                "parameters": serde_json::Value::Object((*tool.input_schema).clone())
7883                            }
7884                        }));
7885                        dispatch.insert(tool_name, client_idx);
7886                    }
7887                }
7888            }
7889            dispatch
7890        };
7891
7892        // Seed messages with history if provided
7893        let mut messages: Vec<serde_json::Value> = Vec::new();
7894        if let Some(hist) = history {
7895            for (role, content) in hist {
7896                messages.push(serde_json::json!({"role": role, "content": content}));
7897            }
7898        }
7899        // Add the current user message
7900        messages.push(serde_json::json!({
7901            "role": "user",
7902            "content": user_message
7903        }));
7904
7905        for turn in 0..agent_def.max_turns {
7906            let response = chat_with_tools(
7907                model,
7908                system,
7909                &messages,
7910                &tools_json,
7911                base_url,
7912                api_key,
7913                agent_def.output_format.as_deref(),
7914            )
7915            .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
7916
7917            match response {
7918                LlmResponse::Text(text) => {
7919                    // Add assistant response to history
7920                    messages.push(serde_json::json!({"role": "assistant", "content": &text}));
7921
7922                    // Build conversation history as list of [role, content] pairs
7923                    let history_list: Vec<VmValue> = messages
7924                        .iter()
7925                        .filter_map(|m| {
7926                            let role = m["role"].as_str()?;
7927                            let content = m["content"].as_str()?;
7928                            Some(VmValue::List(Box::new(vec![
7929                                VmValue::String(Arc::from(role)),
7930                                VmValue::String(Arc::from(content)),
7931                            ])))
7932                        })
7933                        .collect();
7934
7935                    // Agent completed — return result map with history
7936                    let result = VmValue::Map(Box::new(vec![
7937                        (
7938                            Arc::from("response"),
7939                            VmValue::String(Arc::from(text.as_str())),
7940                        ),
7941                        (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
7942                        (Arc::from("history"), VmValue::List(Box::new(history_list))),
7943                    ]));
7944
7945                    // Call on_complete lifecycle hook if defined
7946                    let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
7947                    if let Some(hook) = self.globals.get(&hook_name).cloned() {
7948                        let _ = self.call_value(hook, std::slice::from_ref(&result));
7949                    }
7950
7951                    return Ok(result);
7952                }
7953                LlmResponse::ToolUse(tool_calls) => {
7954                    // Add assistant message with tool calls for context
7955                    let tc_json: Vec<serde_json::Value> = tool_calls
7956                        .iter()
7957                        .map(|tc| {
7958                            serde_json::json!({
7959                                "id": tc.id,
7960                                "type": "function",
7961                                "function": {
7962                                    "name": tc.name,
7963                                    "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
7964                                }
7965                            })
7966                        })
7967                        .collect();
7968                    messages.push(serde_json::json!({
7969                        "role": "assistant",
7970                        "tool_calls": tc_json
7971                    }));
7972
7973                    // Build declared tool names (TL tools + MCP tools)
7974                    #[allow(unused_mut)]
7975                    let mut declared: Vec<String> =
7976                        agent_def.tools.iter().map(|t| t.name.clone()).collect();
7977                    #[cfg(feature = "mcp")]
7978                    {
7979                        for name in mcp_tool_dispatch.keys() {
7980                            declared.push(name.clone());
7981                        }
7982                    }
7983
7984                    // Execute each tool call
7985                    let mut results: Vec<(String, String)> = Vec::new();
7986                    for tc in &tool_calls {
7987                        if !declared.iter().any(|d| d == &tc.name) {
7988                            results.push((
7989                                tc.name.clone(),
7990                                format!("Error: '{}' not in declared tools", tc.name),
7991                            ));
7992                            continue;
7993                        }
7994
7995                        // Try MCP dispatch first, then fall back to TL function lookup
7996                        let result_str;
7997                        #[cfg(feature = "mcp")]
7998                        {
7999                            if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
8000                                let mcp_result = mcp_clients[client_idx]
8001                                    .call_tool(&tc.name, tc.input.clone())
8002                                    .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
8003                                result_str = mcp_result
8004                                    .content
8005                                    .iter()
8006                                    .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
8007                                    .collect::<Vec<_>>()
8008                                    .join("\n");
8009                            } else {
8010                                result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8011                            }
8012                        }
8013                        #[cfg(not(feature = "mcp"))]
8014                        {
8015                            result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8016                        }
8017
8018                        // Call on_tool_call lifecycle hook if defined
8019                        let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
8020                        if let Some(hook) = self.globals.get(&hook_name).cloned() {
8021                            let hook_args = vec![
8022                                VmValue::String(Arc::from(tc.name.as_str())),
8023                                self.json_value_to_vm(&tc.input),
8024                                VmValue::String(Arc::from(result_str.as_str())),
8025                            ];
8026                            let _ = self.call_value(hook, &hook_args);
8027                        }
8028
8029                        results.push((tc.name.clone(), result_str));
8030                    }
8031
8032                    // Format tool results and add to messages
8033                    let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
8034                    messages.extend(result_msgs);
8035                }
8036            }
8037        }
8038
8039        Err(runtime_err(format!(
8040            "Agent '{}' exceeded max_turns ({})",
8041            agent_def.name, agent_def.max_turns
8042        )))
8043    }
8044
8045    #[cfg(feature = "native")]
8046    fn execute_tool_call(
8047        &mut self,
8048        tool_name: &str,
8049        input: &serde_json::Value,
8050    ) -> Result<String, TlError> {
8051        // Look up the tool function in globals
8052        let func = self
8053            .globals
8054            .get(tool_name)
8055            .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8056            .clone();
8057
8058        // Convert JSON args to VmValues
8059        let args = self.json_to_vm_args(input);
8060
8061        // Call the function using call_value
8062        let result = self.call_value(func, &args)?;
8063
8064        // Convert result to string for the LLM
8065        Ok(format!("{result}"))
8066    }
8067
8068    #[cfg(feature = "native")]
8069    fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8070        match input {
8071            serde_json::Value::Object(map) => {
8072                // Pass values in order as positional args
8073                map.values().map(|v| self.json_value_to_vm(v)).collect()
8074            }
8075            serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8076            _ => vec![self.json_value_to_vm(input)],
8077        }
8078    }
8079
8080    #[cfg(feature = "native")]
8081    fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8082        match val {
8083            serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8084            serde_json::Value::Number(n) => {
8085                if let Some(i) = n.as_i64() {
8086                    VmValue::Int(i)
8087                } else if let Some(f) = n.as_f64() {
8088                    VmValue::Float(f)
8089                } else {
8090                    VmValue::None
8091                }
8092            }
8093            serde_json::Value::Bool(b) => VmValue::Bool(*b),
8094            serde_json::Value::Null => VmValue::None,
8095            serde_json::Value::Array(arr) => VmValue::List(Box::new(
8096                arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8097            )),
8098            serde_json::Value::Object(map) => {
8099                let pairs: Vec<(Arc<str>, VmValue)> = map
8100                    .iter()
8101                    .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8102                    .collect();
8103                VmValue::Map(Box::new(pairs))
8104            }
8105        }
8106    }
8107
8108    #[cfg(feature = "native")]
8109    fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8110        match &func {
8111            VmValue::Function(_) => {
8112                // Set up a synthetic call: push args to stack, do_call
8113                let save_len = self.stack.len();
8114                let func_slot = save_len;
8115                let _args_start = func_slot + 1;
8116                self.stack.push(func.clone());
8117                for arg in args {
8118                    self.stack.push(arg.clone());
8119                }
8120                self.ensure_stack(self.stack.len() + 256);
8121
8122                self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8123
8124                // Run until the function returns
8125                let entry_depth = self.frames.len() - 1;
8126                while self.frames.len() > entry_depth {
8127                    if self.run_step(entry_depth)?.is_some() {
8128                        break;
8129                    }
8130                }
8131
8132                // Result is at func_slot
8133                let result = self.stack[func_slot].clone();
8134                self.stack.truncate(save_len);
8135                Ok(result)
8136            }
8137            VmValue::Builtin(id) => {
8138                let id_u16 = *id as u16;
8139                let save_len = self.stack.len();
8140                for arg in args {
8141                    self.stack.push(arg.clone());
8142                }
8143                let result = self.call_builtin(id_u16, save_len, args.len())?;
8144                self.stack.truncate(save_len);
8145                Ok(result)
8146            }
8147            _ => Err(runtime_err(format!(
8148                "Agent tool '{}' is not callable",
8149                func.type_name()
8150            ))),
8151        }
8152    }
8153
8154    #[cfg(feature = "native")]
8155    fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8156        if let Some(dur) = s.strip_prefix("tumbling:") {
8157            let ms = tl_stream::parse_duration(dur).ok()?;
8158            Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8159        } else if let Some(rest) = s.strip_prefix("sliding:") {
8160            let parts: Vec<&str> = rest.splitn(2, ':').collect();
8161            if parts.len() == 2 {
8162                let wms = tl_stream::parse_duration(parts[0]).ok()?;
8163                let sms = tl_stream::parse_duration(parts[1]).ok()?;
8164                Some(tl_stream::window::WindowType::Sliding {
8165                    window_ms: wms,
8166                    slide_ms: sms,
8167                })
8168            } else {
8169                None
8170            }
8171        } else if let Some(dur) = s.strip_prefix("session:") {
8172            let ms = tl_stream::parse_duration(dur).ok()?;
8173            Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8174        } else {
8175            None
8176        }
8177    }
8178
8179    #[cfg(feature = "native")]
8180    fn handle_connector_decl(
8181        &mut self,
8182        frame_idx: usize,
8183        type_const: u8,
8184        config_const: u8,
8185    ) -> Result<VmValue, TlError> {
8186        let frame = &self.frames[frame_idx];
8187        let connector_type = match &frame.prototype.constants[type_const as usize] {
8188            Constant::String(s) => s.to_string(),
8189            _ => return Err(runtime_err("Expected string constant for connector type")),
8190        };
8191
8192        let config_args = match &frame.prototype.constants[config_const as usize] {
8193            Constant::AstExprList(args) => args.clone(),
8194            _ => return Err(runtime_err("Expected AST expr list for connector config")),
8195        };
8196
8197        let mut properties = std::collections::HashMap::new();
8198        for arg in &config_args {
8199            if let AstExpr::NamedArg { name: key, value } = arg {
8200                let val_str = match value.as_ref() {
8201                    AstExpr::String(s) => s.clone(),
8202                    AstExpr::Int(n) => n.to_string(),
8203                    AstExpr::Float(f) => f.to_string(),
8204                    AstExpr::Bool(b) => b.to_string(),
8205                    other => {
8206                        // Try to resolve Ident from globals
8207                        if let AstExpr::Ident(ident) = other {
8208                            if let Some(val) = self.globals.get(ident.as_str()) {
8209                                format!("{val}")
8210                            } else {
8211                                ident.clone()
8212                            }
8213                        } else {
8214                            format!("{other:?}")
8215                        }
8216                    }
8217                };
8218                properties.insert(key.clone(), val_str);
8219            }
8220        }
8221
8222        let config = tl_stream::ConnectorConfig {
8223            name: String::new(), // Will be set by SetGlobal
8224            connector_type,
8225            properties,
8226        };
8227
8228        Ok(VmValue::Connector(Arc::new(config)))
8229    }
8230
8231    /// Advance a generator by one step, returning the next value or None if done.
8232    fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8233        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8234        if gn.done {
8235            return Ok(VmValue::None);
8236        }
8237        match &mut gn.kind {
8238            GeneratorKind::UserDefined {
8239                prototype,
8240                upvalues,
8241                saved_stack,
8242                ip,
8243            } => {
8244                let proto = prototype.clone();
8245                let uvs = upvalues.clone();
8246                let stack_snapshot = saved_stack.clone();
8247                let saved_ip = *ip;
8248                drop(gn); // release lock before running VM
8249
8250                // Set up a frame to resume the generator
8251                let new_base = self.stack.len();
8252                let num_regs = proto.num_registers as usize;
8253                self.ensure_stack(new_base + num_regs + 1);
8254                // Restore saved registers
8255                for (i, val) in stack_snapshot.iter().enumerate() {
8256                    self.stack[new_base + i] = val.clone();
8257                }
8258
8259                self.frames.push(CallFrame {
8260                    prototype: proto,
8261                    ip: saved_ip,
8262                    base: new_base,
8263                    upvalues: uvs,
8264                });
8265
8266                self.yielded_value = None;
8267                let _result = self.run()?;
8268
8269                if let Some(yielded) = self.yielded_value.take() {
8270                    // Generator yielded — save state back
8271                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8272                    if let GeneratorKind::UserDefined {
8273                        saved_stack, ip, ..
8274                    } = &mut gn.kind
8275                    {
8276                        // Save the current register state
8277                        let num_regs_save = saved_stack.len();
8278                        for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8279                            if new_base + i < self.stack.len() {
8280                                *slot = self.stack[new_base + i].clone();
8281                            }
8282                        }
8283                        // Save the ip (instruction after yield)
8284                        *ip = self.yielded_ip;
8285                    }
8286                    self.stack.truncate(new_base);
8287                    Ok(yielded)
8288                } else {
8289                    // Generator returned (no yield) — mark done
8290                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8291                    gn.done = true;
8292                    self.stack.truncate(new_base);
8293                    Ok(VmValue::None)
8294                }
8295            }
8296            GeneratorKind::ListIter { items, index } => {
8297                if *index < items.len() {
8298                    let val = items[*index].clone();
8299                    *index += 1;
8300                    Ok(val)
8301                } else {
8302                    gn.done = true;
8303                    Ok(VmValue::None)
8304                }
8305            }
8306            GeneratorKind::Take { source, remaining } => {
8307                if *remaining == 0 {
8308                    gn.done = true;
8309                    return Ok(VmValue::None);
8310                }
8311                *remaining -= 1;
8312                let src = source.clone();
8313                drop(gn);
8314                let val = self.generator_next(&src)?;
8315                if matches!(val, VmValue::None) {
8316                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8317                    gn.done = true;
8318                }
8319                Ok(val)
8320            }
8321            GeneratorKind::Skip { source, remaining } => {
8322                let src = source.clone();
8323                let skip_n = *remaining;
8324                *remaining = 0;
8325                drop(gn);
8326                // Skip initial values
8327                for _ in 0..skip_n {
8328                    let val = self.generator_next(&src)?;
8329                    if matches!(val, VmValue::None) {
8330                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8331                        gn.done = true;
8332                        return Ok(VmValue::None);
8333                    }
8334                }
8335                let val = self.generator_next(&src)?;
8336                if matches!(val, VmValue::None) {
8337                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8338                    gn.done = true;
8339                }
8340                Ok(val)
8341            }
8342            GeneratorKind::Map { source, func } => {
8343                let src = source.clone();
8344                let f = func.clone();
8345                drop(gn);
8346                let val = self.generator_next(&src)?;
8347                if matches!(val, VmValue::None) {
8348                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8349                    gn.done = true;
8350                    return Ok(VmValue::None);
8351                }
8352                self.call_vm_function(&f, &[val])
8353            }
8354            GeneratorKind::Filter { source, func } => {
8355                let src = source.clone();
8356                let f = func.clone();
8357                drop(gn);
8358                loop {
8359                    let val = self.generator_next(&src)?;
8360                    if matches!(val, VmValue::None) {
8361                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8362                        gn.done = true;
8363                        return Ok(VmValue::None);
8364                    }
8365                    let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8366                    if test.is_truthy() {
8367                        return Ok(val);
8368                    }
8369                }
8370            }
8371            GeneratorKind::Chain {
8372                first,
8373                second,
8374                on_second,
8375            } => {
8376                if !*on_second {
8377                    let src = first.clone();
8378                    drop(gn);
8379                    let val = self.generator_next(&src)?;
8380                    if matches!(val, VmValue::None) {
8381                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8382                        if let GeneratorKind::Chain {
8383                            on_second, second, ..
8384                        } = &mut gn.kind
8385                        {
8386                            *on_second = true;
8387                            let src2 = second.clone();
8388                            drop(gn);
8389                            return self.generator_next(&src2);
8390                        }
8391                    }
8392                    Ok(val)
8393                } else {
8394                    let src = second.clone();
8395                    drop(gn);
8396                    let val = self.generator_next(&src)?;
8397                    if matches!(val, VmValue::None) {
8398                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8399                        gn.done = true;
8400                    }
8401                    Ok(val)
8402                }
8403            }
8404            GeneratorKind::Zip { first, second } => {
8405                let src1 = first.clone();
8406                let src2 = second.clone();
8407                drop(gn);
8408                let val1 = self.generator_next(&src1)?;
8409                let val2 = self.generator_next(&src2)?;
8410                if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8411                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8412                    gn.done = true;
8413                    return Ok(VmValue::None);
8414                }
8415                Ok(VmValue::List(Box::new(vec![val1, val2])))
8416            }
8417            GeneratorKind::Enumerate { source, index } => {
8418                let src = source.clone();
8419                let idx = *index;
8420                *index += 1;
8421                drop(gn);
8422                let val = self.generator_next(&src)?;
8423                if matches!(val, VmValue::None) {
8424                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8425                    gn.done = true;
8426                    return Ok(VmValue::None);
8427                }
8428                Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8429            }
8430        }
8431    }
8432
8433    /// Process a __schema__:Name:vN:fields... global to register in schema_registry.
8434    #[cfg(feature = "native")]
8435    fn process_schema_global(&mut self, s: &str) {
8436        // Format: __schema__:Name:vN:field1:Type,field2:Type,...
8437        let rest = &s["__schema__:".len()..];
8438        let parts: Vec<&str> = rest.splitn(3, ':').collect();
8439        if parts.len() < 2 {
8440            return;
8441        }
8442
8443        let schema_name = parts[0];
8444        let mut version: i64 = 0;
8445        let fields_str;
8446
8447        if parts.len() == 3 && parts[1].starts_with('v') {
8448            // Versioned: Name:vN:fields
8449            version = parts[1][1..].parse().unwrap_or(0);
8450            fields_str = parts[2];
8451        } else if parts.len() == 3 {
8452            // No version prefix, treat as v0: Name:field1:...
8453            fields_str = &rest[schema_name.len() + 1..];
8454        } else {
8455            fields_str = parts[1];
8456        }
8457
8458        if version == 0 {
8459            return;
8460        } // Only register versioned schemas
8461
8462        let mut arrow_fields = Vec::new();
8463        for field_pair in fields_str.split(',') {
8464            let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8465            if kv.len() == 2 {
8466                let fname = kv[0].trim();
8467                let ftype = kv[1].trim();
8468                // Parse type expr debug format: Simple("typename")
8469                let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8470                    &ftype[8..ftype.len() - 2]
8471                } else {
8472                    ftype
8473                };
8474                let dt = crate::schema::type_name_to_arrow_pub(type_name);
8475                arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8476            }
8477        }
8478
8479        if !arrow_fields.is_empty() {
8480            let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8481            let _ = self.schema_registry.register(
8482                schema_name,
8483                version,
8484                schema,
8485                crate::schema::SchemaMetadata::default(),
8486            );
8487        }
8488    }
8489
8490    /// Process a __migrate__:Name:fromVer:toVer:ops global to apply migration.
8491    #[cfg(feature = "native")]
8492    fn process_migrate_global(&mut self, s: &str) {
8493        // Format: __migrate__:Name:from:to:op1;op2;...
8494        let rest = &s["__migrate__:".len()..];
8495        let parts: Vec<&str> = rest.splitn(4, ':').collect();
8496        if parts.len() < 4 {
8497            return;
8498        }
8499
8500        let schema_name = parts[0];
8501        let from_ver: i64 = parts[1].parse().unwrap_or(0);
8502        let to_ver: i64 = parts[2].parse().unwrap_or(0);
8503        let ops_str = parts[3];
8504
8505        let mut ops = Vec::new();
8506        for op_str in ops_str.split(';') {
8507            let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8508            if op_parts.is_empty() {
8509                continue;
8510            }
8511            match op_parts[0] {
8512                "add" if op_parts.len() >= 3 => {
8513                    let name = op_parts[1].to_string();
8514                    // Parse type from debug format: Simple("typename")
8515                    let type_raw = op_parts[2];
8516                    let type_name =
8517                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8518                            type_raw[8..type_raw.len() - 2].to_string()
8519                        } else {
8520                            type_raw.to_string()
8521                        };
8522                    let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
8523                        Some(
8524                            op_parts[3]["default:".len()..]
8525                                .trim_matches('"')
8526                                .to_string(),
8527                        )
8528                    } else {
8529                        None
8530                    };
8531                    ops.push(crate::schema::MigrationOp::AddColumn {
8532                        name,
8533                        type_name,
8534                        default,
8535                    });
8536                }
8537                "drop" if op_parts.len() >= 2 => {
8538                    ops.push(crate::schema::MigrationOp::DropColumn {
8539                        name: op_parts[1].to_string(),
8540                    });
8541                }
8542                "rename" if op_parts.len() >= 3 => {
8543                    ops.push(crate::schema::MigrationOp::RenameColumn {
8544                        from: op_parts[1].to_string(),
8545                        to: op_parts[2].to_string(),
8546                    });
8547                }
8548                "alter" if op_parts.len() >= 3 => {
8549                    let type_raw = op_parts[2];
8550                    let type_name =
8551                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8552                            type_raw[8..type_raw.len() - 2].to_string()
8553                        } else {
8554                            type_raw.to_string()
8555                        };
8556                    ops.push(crate::schema::MigrationOp::AlterType {
8557                        column: op_parts[1].to_string(),
8558                        new_type: type_name,
8559                    });
8560                }
8561                _ => {}
8562            }
8563        }
8564
8565        let _ = self
8566            .schema_registry
8567            .apply_migration(schema_name, from_ver, to_ver, &ops);
8568    }
8569
8570    /// Dispatch a method call on an object.
8571    /// Deep-clone a VmValue, recursively copying containers.
8572    fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
8573        match val {
8574            VmValue::List(items) => {
8575                let cloned: Result<Vec<_>, _> =
8576                    items.iter().map(|v| self.deep_clone_value(v)).collect();
8577                Ok(VmValue::List(Box::new(cloned?)))
8578            }
8579            VmValue::Map(pairs) => {
8580                let cloned: Result<Vec<_>, _> = pairs
8581                    .iter()
8582                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8583                    .collect();
8584                Ok(VmValue::Map(Box::new(cloned?)))
8585            }
8586            VmValue::Set(items) => {
8587                let cloned: Result<Vec<_>, _> =
8588                    items.iter().map(|v| self.deep_clone_value(v)).collect();
8589                Ok(VmValue::Set(Box::new(cloned?)))
8590            }
8591            VmValue::StructInstance(inst) => {
8592                let cloned_fields: Result<Vec<_>, _> = inst
8593                    .fields
8594                    .iter()
8595                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8596                    .collect();
8597                Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
8598                    type_name: inst.type_name.clone(),
8599                    fields: cloned_fields?,
8600                })))
8601            }
8602            VmValue::EnumInstance(e) => {
8603                let cloned_fields: Result<Vec<_>, _> =
8604                    e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
8605                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
8606                    type_name: e.type_name.clone(),
8607                    variant: e.variant.clone(),
8608                    fields: cloned_fields?,
8609                })))
8610            }
8611            #[cfg(feature = "gpu")]
8612            VmValue::GpuTensor(gt) => {
8613                let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
8614                Ok(VmValue::GpuTensor(Arc::new(cloned)))
8615            }
8616            VmValue::Ref(inner) => self.deep_clone_value(inner),
8617            VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
8618            VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
8619            VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
8620            VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
8621            other => Ok(other.clone()),
8622        }
8623    }
8624
8625    pub fn dispatch_method(
8626        &mut self,
8627        obj: VmValue,
8628        method: &str,
8629        args: &[VmValue],
8630    ) -> Result<VmValue, TlError> {
8631        // Universal .clone() method — deep copy any value
8632        if method == "clone" {
8633            return self.deep_clone_value(&obj);
8634        }
8635        // Unwrap Ref for method dispatch — methods can be called through references
8636        let obj = match obj {
8637            VmValue::Ref(inner) => inner.as_ref().clone(),
8638            other => other,
8639        };
8640        match &obj {
8641            VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
8642            VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
8643            VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
8644            VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
8645            VmValue::Module(m) => {
8646                if let Some(func) = m.exports.get(method).cloned() {
8647                    self.call_vm_function(&func, args)
8648                } else {
8649                    Err(runtime_err(format!(
8650                        "Module '{}' has no export '{}'",
8651                        m.name, method
8652                    )))
8653                }
8654            }
8655            VmValue::StructInstance(inst) => {
8656                // Look up impl method: Type::method in globals
8657                let mangled = format!("{}::{}", inst.type_name, method);
8658                if let Some(func) = self.globals.get(&mangled).cloned() {
8659                    let mut all_args = vec![obj.clone()];
8660                    all_args.extend_from_slice(args);
8661                    self.call_vm_function(&func, &all_args)
8662                } else {
8663                    Err(runtime_err(format!(
8664                        "No method '{}' on struct '{}'",
8665                        method, inst.type_name
8666                    )))
8667                }
8668            }
8669            #[cfg(feature = "python")]
8670            VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
8671            #[cfg(feature = "gpu")]
8672            VmValue::GpuTensor(gt) => match method {
8673                "to_cpu" => {
8674                    let cpu = gt.to_cpu().map_err(runtime_err)?;
8675                    Ok(VmValue::Tensor(Arc::new(cpu)))
8676                }
8677                "shape" => {
8678                    let shape_list = Box::new(
8679                        gt.shape
8680                            .iter()
8681                            .map(|&d| VmValue::Int(d as i64))
8682                            .collect::<Vec<_>>(),
8683                    );
8684                    Ok(VmValue::List(shape_list))
8685                }
8686                "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
8687                _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
8688            },
8689            _ => {
8690                // Try looking up Type::method from type_name
8691                let type_name = obj.type_name();
8692                let mangled = format!("{}::{}", type_name, method);
8693                if let Some(func) = self.globals.get(&mangled).cloned() {
8694                    let mut all_args = vec![obj];
8695                    all_args.extend_from_slice(args);
8696                    self.call_vm_function(&func, &all_args)
8697                } else {
8698                    Err(runtime_err(format!(
8699                        "No method '{}' on type '{}'",
8700                        method, type_name
8701                    )))
8702                }
8703            }
8704        }
8705    }
8706
8707    /// Dispatch string methods.
8708    fn dispatch_string_method(
8709        &self,
8710        s: Arc<str>,
8711        method: &str,
8712        args: &[VmValue],
8713    ) -> Result<VmValue, TlError> {
8714        match method {
8715            "len" => Ok(VmValue::Int(s.len() as i64)),
8716            "split" => {
8717                let sep = match args.first() {
8718                    Some(VmValue::String(sep)) => sep.to_string(),
8719                    _ => return Err(runtime_err("split() expects a string separator")),
8720                };
8721                let parts: Vec<VmValue> = s
8722                    .split(&sep)
8723                    .map(|p| VmValue::String(Arc::from(p)))
8724                    .collect();
8725                Ok(VmValue::List(Box::new(parts)))
8726            }
8727            "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
8728            "contains" => {
8729                let needle = match args.first() {
8730                    Some(VmValue::String(n)) => n.to_string(),
8731                    _ => return Err(runtime_err("contains() expects a string")),
8732                };
8733                Ok(VmValue::Bool(s.contains(&needle)))
8734            }
8735            "replace" => {
8736                if args.len() < 2 {
8737                    return Err(runtime_err("replace() expects 2 arguments (old, new)"));
8738                }
8739                let old = match &args[0] {
8740                    VmValue::String(s) => s.to_string(),
8741                    _ => return Err(runtime_err("replace() arg must be string")),
8742                };
8743                let new = match &args[1] {
8744                    VmValue::String(s) => s.to_string(),
8745                    _ => return Err(runtime_err("replace() arg must be string")),
8746                };
8747                Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
8748            }
8749            "starts_with" => {
8750                let prefix = match args.first() {
8751                    Some(VmValue::String(p)) => p.to_string(),
8752                    _ => return Err(runtime_err("starts_with() expects a string")),
8753                };
8754                Ok(VmValue::Bool(s.starts_with(&prefix)))
8755            }
8756            "ends_with" => {
8757                let suffix = match args.first() {
8758                    Some(VmValue::String(p)) => p.to_string(),
8759                    _ => return Err(runtime_err("ends_with() expects a string")),
8760                };
8761                Ok(VmValue::Bool(s.ends_with(&suffix)))
8762            }
8763            "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
8764            "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
8765            "chars" => {
8766                let chars: Vec<VmValue> = s
8767                    .chars()
8768                    .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
8769                    .collect();
8770                Ok(VmValue::List(Box::new(chars)))
8771            }
8772            "repeat" => {
8773                let n = match args.first() {
8774                    Some(VmValue::Int(n)) => *n as usize,
8775                    _ => return Err(runtime_err("repeat() expects an integer")),
8776                };
8777                Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
8778            }
8779            "index_of" => {
8780                let needle = match args.first() {
8781                    Some(VmValue::String(n)) => n.to_string(),
8782                    _ => return Err(runtime_err("index_of() expects a string")),
8783                };
8784                Ok(VmValue::Int(
8785                    s.find(&needle).map(|i| i as i64).unwrap_or(-1),
8786                ))
8787            }
8788            "substring" => {
8789                if args.len() < 2 {
8790                    return Err(runtime_err("substring() expects start and end"));
8791                }
8792                let start = match &args[0] {
8793                    VmValue::Int(n) => *n as usize,
8794                    _ => return Err(runtime_err("substring() expects integers")),
8795                };
8796                let end = match &args[1] {
8797                    VmValue::Int(n) => *n as usize,
8798                    _ => return Err(runtime_err("substring() expects integers")),
8799                };
8800                let end = end.min(s.len());
8801                let start = start.min(end);
8802                Ok(VmValue::String(Arc::from(&s[start..end])))
8803            }
8804            "pad_left" => {
8805                if args.is_empty() {
8806                    return Err(runtime_err("pad_left() expects width"));
8807                }
8808                let width = match &args[0] {
8809                    VmValue::Int(n) => *n as usize,
8810                    _ => return Err(runtime_err("pad_left() expects integer width")),
8811                };
8812                let ch = match args.get(1) {
8813                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8814                    _ => ' ',
8815                };
8816                if s.len() >= width {
8817                    Ok(VmValue::String(s))
8818                } else {
8819                    Ok(VmValue::String(Arc::from(
8820                        format!(
8821                            "{}{}",
8822                            std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
8823                            s
8824                        )
8825                        .as_str(),
8826                    )))
8827                }
8828            }
8829            "pad_right" => {
8830                if args.is_empty() {
8831                    return Err(runtime_err("pad_right() expects width"));
8832                }
8833                let width = match &args[0] {
8834                    VmValue::Int(n) => *n as usize,
8835                    _ => return Err(runtime_err("pad_right() expects integer width")),
8836                };
8837                let ch = match args.get(1) {
8838                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8839                    _ => ' ',
8840                };
8841                if s.len() >= width {
8842                    Ok(VmValue::String(s))
8843                } else {
8844                    Ok(VmValue::String(Arc::from(
8845                        format!(
8846                            "{}{}",
8847                            s,
8848                            std::iter::repeat_n(ch, width - s.len()).collect::<String>()
8849                        )
8850                        .as_str(),
8851                    )))
8852                }
8853            }
8854            "join" => {
8855                // "sep".join(list) -> string
8856                let items = match args.first() {
8857                    Some(VmValue::List(items)) => items,
8858                    _ => return Err(runtime_err("join() expects a list")),
8859                };
8860                let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
8861                Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
8862            }
8863            "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
8864            "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
8865            "count" => {
8866                if args.is_empty() {
8867                    return Err(runtime_err("count() expects a substring"));
8868                }
8869                if let VmValue::String(sub) = &args[0] {
8870                    Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
8871                } else {
8872                    Err(runtime_err("count() expects a string"))
8873                }
8874            }
8875            "is_empty" => Ok(VmValue::Bool(s.is_empty())),
8876            "is_numeric" => Ok(VmValue::Bool(
8877                s.chars()
8878                    .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
8879            )),
8880            "is_alpha" => Ok(VmValue::Bool(
8881                !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
8882            )),
8883            "strip_prefix" => {
8884                if args.is_empty() {
8885                    return Err(runtime_err("strip_prefix() expects a string"));
8886                }
8887                if let VmValue::String(prefix) = &args[0] {
8888                    match s.strip_prefix(prefix.as_ref()) {
8889                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8890                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8891                    }
8892                } else {
8893                    Err(runtime_err("strip_prefix() expects a string"))
8894                }
8895            }
8896            "strip_suffix" => {
8897                if args.is_empty() {
8898                    return Err(runtime_err("strip_suffix() expects a string"));
8899                }
8900                if let VmValue::String(suffix) = &args[0] {
8901                    match s.strip_suffix(suffix.as_ref()) {
8902                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8903                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8904                    }
8905                } else {
8906                    Err(runtime_err("strip_suffix() expects a string"))
8907                }
8908            }
8909            _ => Err(runtime_err(format!("No method '{}' on string", method))),
8910        }
8911    }
8912
8913    /// Dispatch list methods.
8914    fn dispatch_list_method(
8915        &mut self,
8916        items: Vec<VmValue>,
8917        method: &str,
8918        args: &[VmValue],
8919    ) -> Result<VmValue, TlError> {
8920        match method {
8921            "len" => Ok(VmValue::Int(items.len() as i64)),
8922            "push" => {
8923                if args.is_empty() {
8924                    return Err(runtime_err("push() expects 1 argument"));
8925                }
8926                let mut new_items = items;
8927                new_items.push(args[0].clone());
8928                Ok(VmValue::List(Box::new(new_items)))
8929            }
8930            "map" => {
8931                if args.is_empty() {
8932                    return Err(runtime_err("map() expects a function"));
8933                }
8934                let func = &args[0];
8935                let mut result = Vec::new();
8936                for item in items {
8937                    let val = self.call_vm_function(func, &[item])?;
8938                    result.push(val);
8939                }
8940                Ok(VmValue::List(Box::new(result)))
8941            }
8942            "filter" => {
8943                if args.is_empty() {
8944                    return Err(runtime_err("filter() expects a function"));
8945                }
8946                let func = &args[0];
8947                let mut result = Vec::new();
8948                for item in items {
8949                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
8950                    if val.is_truthy() {
8951                        result.push(item);
8952                    }
8953                }
8954                Ok(VmValue::List(Box::new(result)))
8955            }
8956            "reduce" => {
8957                if args.len() < 2 {
8958                    return Err(runtime_err("reduce() expects initial value and function"));
8959                }
8960                let mut acc = args[0].clone();
8961                let func = &args[1];
8962                for item in items {
8963                    acc = self.call_vm_function(func, &[acc, item])?;
8964                }
8965                Ok(acc)
8966            }
8967            "sort" => {
8968                let mut sorted = items;
8969                sorted.sort_by(|a, b| match (a, b) {
8970                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
8971                    (VmValue::Float(x), VmValue::Float(y)) => {
8972                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
8973                    }
8974                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
8975                    _ => std::cmp::Ordering::Equal,
8976                });
8977                Ok(VmValue::List(Box::new(sorted)))
8978            }
8979            "reverse" => {
8980                let mut reversed = items;
8981                reversed.reverse();
8982                Ok(VmValue::List(Box::new(reversed)))
8983            }
8984            "contains" => {
8985                if args.is_empty() {
8986                    return Err(runtime_err("contains() expects a value"));
8987                }
8988                let needle = &args[0];
8989                let found = items.iter().any(|item| match (item, needle) {
8990                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
8991                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
8992                    (VmValue::String(a), VmValue::String(b)) => a == b,
8993                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8994                    (VmValue::None, VmValue::None) => true,
8995                    _ => false,
8996                });
8997                Ok(VmValue::Bool(found))
8998            }
8999            "index_of" => {
9000                if args.is_empty() {
9001                    return Err(runtime_err("index_of() expects a value"));
9002                }
9003                let needle = &args[0];
9004                let idx = items.iter().position(|item| match (item, needle) {
9005                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
9006                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
9007                    (VmValue::String(a), VmValue::String(b)) => a == b,
9008                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9009                    (VmValue::None, VmValue::None) => true,
9010                    _ => false,
9011                });
9012                Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
9013            }
9014            "slice" => {
9015                if args.len() < 2 {
9016                    return Err(runtime_err("slice() expects start and end"));
9017                }
9018                let start = match &args[0] {
9019                    VmValue::Int(n) => *n as usize,
9020                    _ => return Err(runtime_err("slice() expects integers")),
9021                };
9022                let end = match &args[1] {
9023                    VmValue::Int(n) => *n as usize,
9024                    _ => return Err(runtime_err("slice() expects integers")),
9025                };
9026                let end = end.min(items.len());
9027                let start = start.min(end);
9028                Ok(VmValue::List(Box::new(items[start..end].to_vec())))
9029            }
9030            "flat_map" => {
9031                if args.is_empty() {
9032                    return Err(runtime_err("flat_map() expects a function"));
9033                }
9034                let func = &args[0];
9035                let mut result = Vec::new();
9036                for item in items {
9037                    let val = self.call_vm_function(func, &[item])?;
9038                    match val {
9039                        VmValue::List(sub) => result.extend(*sub),
9040                        other => result.push(other),
9041                    }
9042                }
9043                Ok(VmValue::List(Box::new(result)))
9044            }
9045            "find" => {
9046                if args.is_empty() {
9047                    return Err(runtime_err("find() expects a predicate function"));
9048                }
9049                let func = &args[0];
9050                for item in items {
9051                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9052                    if val.is_truthy() {
9053                        return Ok(item);
9054                    }
9055                }
9056                Ok(VmValue::None)
9057            }
9058            "sort_by" => {
9059                if args.is_empty() {
9060                    return Err(runtime_err("sort_by() expects a key function"));
9061                }
9062                let func = &args[0];
9063                let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9064                for item in items {
9065                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9066                    keyed.push((key, item));
9067                }
9068                keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9069                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9070                    (VmValue::Float(x), VmValue::Float(y)) => {
9071                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9072                    }
9073                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9074                    _ => std::cmp::Ordering::Equal,
9075                });
9076                Ok(VmValue::List(Box::new(
9077                    keyed.into_iter().map(|(_, v)| v).collect(),
9078                )))
9079            }
9080            "group_by" => {
9081                if args.is_empty() {
9082                    return Err(runtime_err("group_by() expects a key function"));
9083                }
9084                let func = &args[0];
9085                let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9086                for item in items {
9087                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9088                    let key_str: Arc<str> = match &key {
9089                        VmValue::String(s) => s.clone(),
9090                        other => Arc::from(format!("{other}").as_str()),
9091                    };
9092                    if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9093                        group.1.push(item);
9094                    } else {
9095                        groups.push((key_str, vec![item]));
9096                    }
9097                }
9098                let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9099                    .into_iter()
9100                    .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9101                    .collect();
9102                Ok(VmValue::Map(Box::new(map_pairs)))
9103            }
9104            "unique" => {
9105                let mut seen = Vec::new();
9106                let mut result = Vec::new();
9107                for item in &items {
9108                    let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9109                    if !is_dup {
9110                        seen.push(item.clone());
9111                        result.push(item.clone());
9112                    }
9113                }
9114                Ok(VmValue::List(Box::new(result)))
9115            }
9116            "flatten" => {
9117                let mut result = Vec::new();
9118                for item in items {
9119                    match item {
9120                        VmValue::List(sub) => result.extend(*sub),
9121                        other => result.push(other),
9122                    }
9123                }
9124                Ok(VmValue::List(Box::new(result)))
9125            }
9126            "chunk" => {
9127                if args.is_empty() {
9128                    return Err(runtime_err("chunk() expects a size"));
9129                }
9130                let n = match &args[0] {
9131                    VmValue::Int(n) if *n > 0 => *n as usize,
9132                    _ => return Err(runtime_err("chunk() expects a positive integer")),
9133                };
9134                let chunks: Vec<VmValue> = items
9135                    .chunks(n)
9136                    .map(|c| VmValue::List(Box::new(c.to_vec())))
9137                    .collect();
9138                Ok(VmValue::List(Box::new(chunks)))
9139            }
9140            "insert" => {
9141                if args.len() < 2 {
9142                    return Err(runtime_err("insert() expects index and value"));
9143                }
9144                let idx = match &args[0] {
9145                    VmValue::Int(n) => *n as usize,
9146                    _ => return Err(runtime_err("insert() expects integer index")),
9147                };
9148                let mut new_items = items;
9149                if idx > new_items.len() {
9150                    return Err(runtime_err("insert() index out of bounds"));
9151                }
9152                new_items.insert(idx, args[1].clone());
9153                Ok(VmValue::List(Box::new(new_items)))
9154            }
9155            "remove_at" => {
9156                if args.is_empty() {
9157                    return Err(runtime_err("remove_at() expects an index"));
9158                }
9159                let idx = match &args[0] {
9160                    VmValue::Int(n) => *n as usize,
9161                    _ => return Err(runtime_err("remove_at() expects integer index")),
9162                };
9163                let mut new_items = items;
9164                if idx >= new_items.len() {
9165                    return Err(runtime_err("remove_at() index out of bounds"));
9166                }
9167                let removed = new_items.remove(idx);
9168                Ok(removed)
9169            }
9170            "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9171            "sum" => {
9172                let mut int_sum: i64 = 0;
9173                let mut has_float = false;
9174                let mut float_sum: f64 = 0.0;
9175                for item in &items {
9176                    match item {
9177                        VmValue::Int(n) => {
9178                            if has_float {
9179                                float_sum += *n as f64;
9180                            } else {
9181                                int_sum += n;
9182                            }
9183                        }
9184                        VmValue::Float(f) => {
9185                            if !has_float {
9186                                has_float = true;
9187                                float_sum = int_sum as f64;
9188                            }
9189                            float_sum += f;
9190                        }
9191                        _ => return Err(runtime_err("sum() requires numeric list")),
9192                    }
9193                }
9194                if has_float {
9195                    Ok(VmValue::Float(float_sum))
9196                } else {
9197                    Ok(VmValue::Int(int_sum))
9198                }
9199            }
9200            "min" => {
9201                if items.is_empty() {
9202                    return Ok(VmValue::None);
9203                }
9204                let mut min_val = items[0].clone();
9205                for item in &items[1..] {
9206                    match (&min_val, item) {
9207                        (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9208                        (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9209                        _ => {}
9210                    }
9211                }
9212                Ok(min_val)
9213            }
9214            "max" => {
9215                if items.is_empty() {
9216                    return Ok(VmValue::None);
9217                }
9218                let mut max_val = items[0].clone();
9219                for item in &items[1..] {
9220                    match (&max_val, item) {
9221                        (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9222                        (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9223                        _ => {}
9224                    }
9225                }
9226                Ok(max_val)
9227            }
9228            "each" => {
9229                if args.is_empty() {
9230                    return Err(runtime_err("each() expects a function"));
9231                }
9232                let func = &args[0];
9233                for item in items {
9234                    self.call_vm_function(func, &[item])?;
9235                }
9236                Ok(VmValue::None)
9237            }
9238            "zip" => {
9239                if args.is_empty() {
9240                    return Err(runtime_err("zip() expects a list"));
9241                }
9242                let other = match &args[0] {
9243                    VmValue::List(other) => other.as_slice(),
9244                    _ => return Err(runtime_err("zip() expects a list")),
9245                };
9246                let len = items.len().min(other.len());
9247                let zipped: Vec<VmValue> = items[..len]
9248                    .iter()
9249                    .zip(other[..len].iter())
9250                    .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9251                    .collect();
9252                Ok(VmValue::List(Box::new(zipped)))
9253            }
9254            "join" => {
9255                let sep = match args.first() {
9256                    Some(VmValue::String(s)) => s.as_ref(),
9257                    _ => "",
9258                };
9259                let s: String = items
9260                    .iter()
9261                    .map(|v| format!("{v}"))
9262                    .collect::<Vec<_>>()
9263                    .join(sep);
9264                Ok(VmValue::String(Arc::from(s.as_str())))
9265            }
9266            _ => Err(runtime_err(format!("No method '{}' on list", method))),
9267        }
9268    }
9269
9270    /// Dispatch map methods.
9271    fn dispatch_map_method(
9272        &mut self,
9273        pairs: Vec<(Arc<str>, VmValue)>,
9274        method: &str,
9275        args: &[VmValue],
9276    ) -> Result<VmValue, TlError> {
9277        match method {
9278            "len" => Ok(VmValue::Int(pairs.len() as i64)),
9279            "keys" => Ok(VmValue::List(Box::new(
9280                pairs
9281                    .iter()
9282                    .map(|(k, _)| VmValue::String(k.clone()))
9283                    .collect(),
9284            ))),
9285            "values" => Ok(VmValue::List(Box::new(
9286                pairs.iter().map(|(_, v)| v.clone()).collect(),
9287            ))),
9288            "contains_key" => {
9289                if args.is_empty() {
9290                    return Err(runtime_err("contains_key() expects a key"));
9291                }
9292                if let VmValue::String(key) = &args[0] {
9293                    Ok(VmValue::Bool(
9294                        pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9295                    ))
9296                } else {
9297                    Err(runtime_err("contains_key() expects a string key"))
9298                }
9299            }
9300            "remove" => {
9301                if args.is_empty() {
9302                    return Err(runtime_err("remove() expects a key"));
9303                }
9304                if let VmValue::String(key) = &args[0] {
9305                    let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9306                        .into_iter()
9307                        .filter(|(k, _)| k.as_ref() != key.as_ref())
9308                        .collect();
9309                    Ok(VmValue::Map(Box::new(new_pairs)))
9310                } else {
9311                    Err(runtime_err("remove() expects a string key"))
9312                }
9313            }
9314            "get" => {
9315                if args.is_empty() {
9316                    return Err(runtime_err("get() expects a key"));
9317                }
9318                if let VmValue::String(key) = &args[0] {
9319                    let default = args.get(1).cloned().unwrap_or(VmValue::None);
9320                    let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9321                    Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9322                } else {
9323                    Err(runtime_err("get() expects a string key"))
9324                }
9325            }
9326            "merge" => {
9327                if args.is_empty() {
9328                    return Err(runtime_err("merge() expects a map"));
9329                }
9330                if let VmValue::Map(other) = &args[0] {
9331                    let mut merged = pairs;
9332                    for (k, v) in other.iter() {
9333                        if let Some(existing) =
9334                            merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9335                        {
9336                            existing.1 = v.clone();
9337                        } else {
9338                            merged.push((k.clone(), v.clone()));
9339                        }
9340                    }
9341                    Ok(VmValue::Map(Box::new(merged)))
9342                } else {
9343                    Err(runtime_err("merge() expects a map"))
9344                }
9345            }
9346            "entries" => {
9347                let entries: Vec<VmValue> = pairs
9348                    .iter()
9349                    .map(|(k, v)| {
9350                        VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9351                    })
9352                    .collect();
9353                Ok(VmValue::List(Box::new(entries)))
9354            }
9355            "map_values" => {
9356                if args.is_empty() {
9357                    return Err(runtime_err("map_values() expects a function"));
9358                }
9359                let func = &args[0];
9360                let mut result = Vec::new();
9361                for (k, v) in pairs {
9362                    let new_v = self.call_vm_function(func, &[v])?;
9363                    result.push((k, new_v));
9364                }
9365                Ok(VmValue::Map(Box::new(result)))
9366            }
9367            "filter" => {
9368                if args.is_empty() {
9369                    return Err(runtime_err("filter() expects a predicate function"));
9370                }
9371                let func = &args[0];
9372                let mut result = Vec::new();
9373                for (k, v) in pairs {
9374                    let val =
9375                        self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9376                    if val.is_truthy() {
9377                        result.push((k, v));
9378                    }
9379                }
9380                Ok(VmValue::Map(Box::new(result)))
9381            }
9382            "set" => {
9383                if args.len() < 2 {
9384                    return Err(runtime_err("set() expects key and value"));
9385                }
9386                if let VmValue::String(key) = &args[0] {
9387                    let mut new_pairs = pairs;
9388                    if let Some(existing) = new_pairs
9389                        .iter_mut()
9390                        .find(|(k, _)| k.as_ref() == key.as_ref())
9391                    {
9392                        existing.1 = args[1].clone();
9393                    } else {
9394                        new_pairs.push((key.clone(), args[1].clone()));
9395                    }
9396                    Ok(VmValue::Map(Box::new(new_pairs)))
9397                } else {
9398                    Err(runtime_err("set() expects a string key"))
9399                }
9400            }
9401            "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9402            _ => Err(runtime_err(format!("No method '{}' on map", method))),
9403        }
9404    }
9405
9406    /// Dispatch set methods.
9407    fn dispatch_set_method(
9408        &self,
9409        items: Vec<VmValue>,
9410        method: &str,
9411        args: &[VmValue],
9412    ) -> Result<VmValue, TlError> {
9413        match method {
9414            "len" => Ok(VmValue::Int(items.len() as i64)),
9415            "contains" => {
9416                if args.is_empty() {
9417                    return Err(runtime_err("contains() expects a value"));
9418                }
9419                Ok(VmValue::Bool(
9420                    items.iter().any(|x| vm_values_equal(x, &args[0])),
9421                ))
9422            }
9423            "add" => {
9424                if args.is_empty() {
9425                    return Err(runtime_err("add() expects a value"));
9426                }
9427                let mut new_items = items;
9428                if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9429                    new_items.push(args[0].clone());
9430                }
9431                Ok(VmValue::Set(Box::new(new_items)))
9432            }
9433            "remove" => {
9434                if args.is_empty() {
9435                    return Err(runtime_err("remove() expects a value"));
9436                }
9437                let new_items: Vec<VmValue> = items
9438                    .into_iter()
9439                    .filter(|x| !vm_values_equal(x, &args[0]))
9440                    .collect();
9441                Ok(VmValue::Set(Box::new(new_items)))
9442            }
9443            "to_list" => Ok(VmValue::List(Box::new(items))),
9444            "union" => {
9445                if args.is_empty() {
9446                    return Err(runtime_err("union() expects a set"));
9447                }
9448                if let VmValue::Set(b) = &args[0] {
9449                    let mut result = items;
9450                    for item in b.iter() {
9451                        if !result.iter().any(|x| vm_values_equal(x, item)) {
9452                            result.push(item.clone());
9453                        }
9454                    }
9455                    Ok(VmValue::Set(Box::new(result)))
9456                } else {
9457                    Err(runtime_err("union() expects a set"))
9458                }
9459            }
9460            "intersection" => {
9461                if args.is_empty() {
9462                    return Err(runtime_err("intersection() expects a set"));
9463                }
9464                if let VmValue::Set(b) = &args[0] {
9465                    let result: Vec<VmValue> = items
9466                        .into_iter()
9467                        .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9468                        .collect();
9469                    Ok(VmValue::Set(Box::new(result)))
9470                } else {
9471                    Err(runtime_err("intersection() expects a set"))
9472                }
9473            }
9474            "difference" => {
9475                if args.is_empty() {
9476                    return Err(runtime_err("difference() expects a set"));
9477                }
9478                if let VmValue::Set(b) = &args[0] {
9479                    let result: Vec<VmValue> = items
9480                        .into_iter()
9481                        .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9482                        .collect();
9483                    Ok(VmValue::Set(Box::new(result)))
9484                } else {
9485                    Err(runtime_err("difference() expects a set"))
9486                }
9487            }
9488            _ => Err(runtime_err(format!("No method '{}' on set", method))),
9489        }
9490    }
9491
9492    /// Handle import at runtime.
9493    #[cfg(feature = "native")]
9494    fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9495        // Resolve relative path from current file
9496        let resolved = if let Some(ref base) = self.file_path {
9497            let base_dir = std::path::Path::new(base)
9498                .parent()
9499                .unwrap_or(std::path::Path::new("."));
9500            let candidate = base_dir.join(path);
9501            if candidate.exists() {
9502                candidate.to_string_lossy().to_string()
9503            } else {
9504                path.to_string()
9505            }
9506        } else {
9507            path.to_string()
9508        };
9509
9510        // Circular dependency detection
9511        if self.importing_files.contains(&resolved) {
9512            return Err(runtime_err(format!("Circular import detected: {resolved}")));
9513        }
9514
9515        // Check module cache
9516        if let Some(exports) = self.module_cache.get(&resolved) {
9517            let exports = exports.clone();
9518            return self.bind_import_exports(exports, alias);
9519        }
9520
9521        // Read, parse, compile, execute the file
9522        let source = std::fs::read_to_string(&resolved)
9523            .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
9524        let program = tl_parser::parse(&source)
9525            .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
9526        let proto = crate::compiler::compile(&program)
9527            .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
9528
9529        // Track circular imports
9530        self.importing_files.insert(resolved.clone());
9531
9532        // Execute in a fresh VM with shared globals
9533        let mut import_vm = Vm::new();
9534        import_vm.file_path = Some(resolved.clone());
9535        import_vm.globals = self.globals.clone();
9536        import_vm.importing_files = self.importing_files.clone();
9537        import_vm.module_cache = self.module_cache.clone();
9538        import_vm.package_roots = self.package_roots.clone();
9539        import_vm.project_root = self.project_root.clone();
9540        import_vm.execute(&proto)?;
9541
9542        self.importing_files.remove(&resolved);
9543
9544        // Collect exports: both globals and top-level locals from the stack
9545        let mut exports = HashMap::new();
9546
9547        // 1. New globals defined in the import
9548        for (k, v) in &import_vm.globals {
9549            if !self.globals.contains_key(k) {
9550                exports.insert(k.clone(), v.clone());
9551            }
9552        }
9553
9554        // 2. Top-level locals from the prototype (on the stack)
9555        for (name, reg) in &proto.top_level_locals {
9556            if !name.starts_with("__enum_") && !exports.contains_key(name) {
9557                let stack_idx = reg;
9558                if (*stack_idx as usize) < import_vm.stack.len() {
9559                    let val = import_vm.stack[*stack_idx as usize].clone();
9560                    if !matches!(val, VmValue::None) || name.starts_with("_") {
9561                        exports.insert(name.clone(), val);
9562                    }
9563                }
9564            }
9565        }
9566
9567        // Cache the module
9568        self.module_cache.insert(resolved, exports.clone());
9569        // Also adopt any modules the sub-VM discovered
9570        for (k, v) in import_vm.module_cache {
9571            self.module_cache.entry(k).or_insert(v);
9572        }
9573
9574        self.bind_import_exports(exports, alias)
9575    }
9576
9577    /// Bind import exports into current scope.
9578    #[cfg(feature = "native")]
9579    fn bind_import_exports(
9580        &mut self,
9581        exports: HashMap<String, VmValue>,
9582        alias: &str,
9583    ) -> Result<VmValue, TlError> {
9584        if alias.is_empty() {
9585            // Wildcard import: merge all exports into current scope
9586            for (k, v) in &exports {
9587                self.globals.insert(k.clone(), v.clone());
9588            }
9589            Ok(VmValue::None)
9590        } else {
9591            // Namespaced import
9592            let module = VmModule {
9593                name: Arc::from(alias),
9594                exports,
9595            };
9596            let module_val = VmValue::Module(Arc::new(module));
9597            self.globals.insert(alias.to_string(), module_val.clone());
9598            Ok(module_val)
9599        }
9600    }
9601
9602    /// Handle use-style imports (dot-path syntax).
9603    #[cfg(feature = "native")]
9604    fn handle_use_import(
9605        &mut self,
9606        path_str: &str,
9607        extra_a: u8,
9608        kind: u8,
9609        _frame_idx: usize,
9610    ) -> Result<VmValue, TlError> {
9611        match kind {
9612            0 => {
9613                // Single: "data.transforms.clean" → import file, bind last segment
9614                let segments: Vec<&str> = path_str.split('.').collect();
9615                let file_path = self.resolve_use_path(&segments)?;
9616                // Import the module, get exports
9617                let _last = segments.last().copied().unwrap_or("");
9618                self.handle_import(&file_path, "")?;
9619                // The wildcard import already merged everything.
9620                // But for Single, we only want the specific item.
9621                // Since handle_import merges all, that works for now.
9622                // Return none since it's a statement, not an expression.
9623                Ok(VmValue::None)
9624            }
9625            1 => {
9626                // Group: "data.transforms.{a,b}" — extract prefix before {
9627                let brace_start = path_str.find('{').unwrap_or(path_str.len());
9628                let prefix = path_str[..brace_start].trim_end_matches('.');
9629                let segments: Vec<&str> = prefix.split('.').collect();
9630                let file_path = self.resolve_use_path(&segments)?;
9631                self.handle_import(&file_path, "")?;
9632                Ok(VmValue::None)
9633            }
9634            2 => {
9635                // Wildcard: "data.transforms.*" — strip trailing .*
9636                let prefix = path_str.trim_end_matches(".*");
9637                let segments: Vec<&str> = prefix.split('.').collect();
9638                let file_path = self.resolve_use_path(&segments)?;
9639                self.handle_import(&file_path, "")?;
9640                Ok(VmValue::None)
9641            }
9642            3 => {
9643                // Aliased: path in path_str, alias in extra_a (constant index)
9644                let segments: Vec<&str> = path_str.split('.').collect();
9645                let file_path = self.resolve_use_path(&segments)?;
9646                // For aliased, we need to get the alias from the constant pool
9647                // extra_a contains the constant index of the alias string
9648                let alias_str = if let Some(frame) = self.frames.last() {
9649                    if let Some(crate::chunk::Constant::String(s)) =
9650                        frame.prototype.constants.get(extra_a as usize)
9651                    {
9652                        s.to_string()
9653                    } else {
9654                        segments.last().copied().unwrap_or("module").to_string()
9655                    }
9656                } else {
9657                    segments.last().copied().unwrap_or("module").to_string()
9658                };
9659                self.handle_import(&file_path, &alias_str)?;
9660                Ok(VmValue::None)
9661            }
9662            _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
9663        }
9664    }
9665
9666    /// Resolve dot-path segments to a file path for use statements.
9667    #[cfg(feature = "native")]
9668    fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
9669        // Reject path traversal attempts
9670        if segments.contains(&"..") {
9671            return Err(runtime_err("Import paths cannot contain '..'"));
9672        }
9673
9674        let base_dir = if let Some(ref fp) = self.file_path {
9675            std::path::Path::new(fp)
9676                .parent()
9677                .unwrap_or(std::path::Path::new("."))
9678                .to_path_buf()
9679        } else {
9680            std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
9681        };
9682
9683        let rel_path = segments.join("/");
9684
9685        // Try file module first
9686        let file_path = base_dir.join(format!("{rel_path}.tl"));
9687        if file_path.exists() {
9688            return Ok(file_path.to_string_lossy().to_string());
9689        }
9690
9691        // Try directory module
9692        let dir_path = base_dir.join(&rel_path).join("mod.tl");
9693        if dir_path.exists() {
9694            return Ok(dir_path.to_string_lossy().to_string());
9695        }
9696
9697        // If multi-segment, try parent as file module
9698        if segments.len() > 1 {
9699            let parent = &segments[..segments.len() - 1];
9700            let parent_path = parent.join("/");
9701            let parent_file = base_dir.join(format!("{parent_path}.tl"));
9702            if parent_file.exists() {
9703                return Ok(parent_file.to_string_lossy().to_string());
9704            }
9705            let parent_dir = base_dir.join(&parent_path).join("mod.tl");
9706            if parent_dir.exists() {
9707                return Ok(parent_dir.to_string_lossy().to_string());
9708            }
9709        }
9710
9711        // Package import fallback: first segment as package name
9712        // Convert underscores to hyphens (TL identifiers use _, package names use -)
9713        let pkg_name_underscore = segments[0];
9714        let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
9715        let pkg_root = self
9716            .package_roots
9717            .get(pkg_name_underscore)
9718            .or_else(|| self.package_roots.get(&pkg_name_hyphen));
9719
9720        if let Some(root) = pkg_root {
9721            let remaining = &segments[1..];
9722            if let Some(path) = resolve_package_file(root, remaining) {
9723                return Ok(path);
9724            }
9725        }
9726
9727        Err(runtime_err(format!(
9728            "Module not found: `{}`",
9729            segments.join(".")
9730        )))
9731    }
9732
9733    /// Call a VmValue function/closure with args.
9734    fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
9735        match func {
9736            VmValue::Function(closure) => {
9737                let proto = closure.prototype.clone();
9738                let arity = proto.arity as usize;
9739                if args.len() != arity {
9740                    return Err(runtime_err(format!(
9741                        "Expected {} arguments, got {}",
9742                        arity,
9743                        args.len()
9744                    )));
9745                }
9746
9747                // If this is a generator function, create a Generator
9748                if proto.is_generator {
9749                    let mut closed_upvalues = Vec::new();
9750                    for uv in &closure.upvalues {
9751                        match uv {
9752                            UpvalueRef::Open { stack_index } => {
9753                                let val = self.stack[*stack_index].clone();
9754                                closed_upvalues.push(UpvalueRef::Closed(val));
9755                            }
9756                            UpvalueRef::Closed(v) => {
9757                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
9758                            }
9759                        }
9760                    }
9761                    let num_regs = proto.num_registers as usize;
9762                    let mut saved_stack = vec![VmValue::None; num_regs];
9763                    for (i, arg) in args.iter().enumerate() {
9764                        saved_stack[i] = arg.clone();
9765                    }
9766                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
9767                        prototype: proto,
9768                        upvalues: closed_upvalues,
9769                        saved_stack,
9770                        ip: 0,
9771                    });
9772                    return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
9773                }
9774
9775                let new_base = self.stack.len();
9776                self.ensure_stack(new_base + proto.num_registers as usize + 1);
9777
9778                for (i, arg) in args.iter().enumerate() {
9779                    self.stack[new_base + i] = arg.clone();
9780                }
9781
9782                self.frames.push(CallFrame {
9783                    prototype: proto,
9784                    ip: 0,
9785                    base: new_base,
9786                    upvalues: closure.upvalues.clone(),
9787                });
9788
9789                let result = self.run()?;
9790                self.stack.truncate(new_base);
9791                Ok(result)
9792            }
9793            VmValue::Builtin(id) => {
9794                // Put args on stack temporarily
9795                let args_base = self.stack.len();
9796                for arg in args {
9797                    self.stack.push(arg.clone());
9798                }
9799                let result = self.call_builtin(*id as u16, args_base, args.len());
9800                self.stack.truncate(args_base);
9801                result
9802            }
9803            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
9804        }
9805    }
9806
9807    // ── Table pipe handler ──
9808
9809    #[cfg(feature = "native")]
9810    fn handle_table_pipe(
9811        &mut self,
9812        frame_idx: usize,
9813        table_val: VmValue,
9814        op_const: u8,
9815        args_const: u8,
9816    ) -> Result<VmValue, TlError> {
9817        let df = match table_val {
9818            VmValue::Table(t) => t.df,
9819            other => {
9820                // Not a table — fall back to regular builtin/function call
9821                return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
9822            }
9823        };
9824
9825        let frame = &self.frames[frame_idx];
9826        let op_name = match &frame.prototype.constants[op_const as usize] {
9827            Constant::String(s) => s.to_string(),
9828            _ => return Err(runtime_err("Expected string constant for table op")),
9829        };
9830        let ast_args = match &frame.prototype.constants[args_const as usize] {
9831            Constant::AstExprList(args) => args.clone(),
9832            _ => return Err(runtime_err("Expected AST expr list for table args")),
9833        };
9834
9835        let ctx = self.build_translate_context();
9836
9837        match op_name.as_str() {
9838            "filter" => {
9839                if ast_args.len() != 1 {
9840                    return Err(runtime_err("filter() expects 1 argument (predicate)"));
9841                }
9842                let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
9843                let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
9844                Ok(VmValue::Table(VmTable { df: filtered }))
9845            }
9846            "select" => {
9847                if ast_args.is_empty() {
9848                    return Err(runtime_err("select() expects at least 1 argument"));
9849                }
9850                let mut select_exprs = Vec::new();
9851                for arg in &ast_args {
9852                    match arg {
9853                        AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
9854                        AstExpr::NamedArg { name, value } => {
9855                            let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9856                            select_exprs.push(expr.alias(name));
9857                        }
9858                        AstExpr::String(name) => select_exprs.push(col(name.as_str())),
9859                        other => {
9860                            let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9861                            select_exprs.push(expr);
9862                        }
9863                    }
9864                }
9865                let selected = df
9866                    .select(select_exprs)
9867                    .map_err(|e| runtime_err(format!("{e}")))?;
9868                Ok(VmValue::Table(VmTable { df: selected }))
9869            }
9870            "sort" => {
9871                if ast_args.is_empty() {
9872                    return Err(runtime_err("sort() expects at least 1 argument (column)"));
9873                }
9874                let mut sort_exprs = Vec::new();
9875                let mut i = 0;
9876                while i < ast_args.len() {
9877                    let col_name = match &ast_args[i] {
9878                        AstExpr::Ident(name) => name.clone(),
9879                        AstExpr::String(name) => name.clone(),
9880                        _ => {
9881                            return Err(runtime_err(
9882                                "sort() column must be an identifier or string",
9883                            ));
9884                        }
9885                    };
9886                    i += 1;
9887                    let ascending = if i < ast_args.len() {
9888                        match &ast_args[i] {
9889                            AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
9890                                i += 1;
9891                                false
9892                            }
9893                            AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
9894                                i += 1;
9895                                true
9896                            }
9897                            _ => true,
9898                        }
9899                    } else {
9900                        true
9901                    };
9902                    sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
9903                }
9904                let sorted = df
9905                    .sort(sort_exprs)
9906                    .map_err(|e| runtime_err(format!("{e}")))?;
9907                Ok(VmValue::Table(VmTable { df: sorted }))
9908            }
9909            "with" => {
9910                if ast_args.len() != 1 {
9911                    return Err(runtime_err(
9912                        "with() expects 1 argument (map of column definitions)",
9913                    ));
9914                }
9915                let pairs = match &ast_args[0] {
9916                    AstExpr::Map(pairs) => pairs,
9917                    _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
9918                };
9919                let mut result_df = df;
9920                for (key, value_expr) in pairs {
9921                    let col_name = match key {
9922                        AstExpr::String(s) => s.clone(),
9923                        AstExpr::Ident(s) => s.clone(),
9924                        _ => return Err(runtime_err("with() key must be a string or identifier")),
9925                    };
9926                    let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
9927                    result_df = result_df
9928                        .with_column(&col_name, df_expr)
9929                        .map_err(|e| runtime_err(format!("{e}")))?;
9930                }
9931                Ok(VmValue::Table(VmTable { df: result_df }))
9932            }
9933            "aggregate" => {
9934                let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9935                let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9936                for arg in &ast_args {
9937                    match arg {
9938                        AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
9939                            AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
9940                            AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
9941                            AstExpr::List(items) => {
9942                                for item in items {
9943                                    match item {
9944                                        AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
9945                                        AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
9946                                        _ => {
9947                                            return Err(runtime_err(
9948                                                "by: list items must be strings or identifiers",
9949                                            ));
9950                                        }
9951                                    }
9952                                }
9953                            }
9954                            _ => return Err(runtime_err("by: must be a column name or list")),
9955                        },
9956                        AstExpr::NamedArg { name, value } => {
9957                            let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9958                            agg_exprs.push(agg_expr.alias(name));
9959                        }
9960                        other => {
9961                            let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9962                            agg_exprs.push(agg_expr);
9963                        }
9964                    }
9965                }
9966                let aggregated = df
9967                    .aggregate(group_by_cols, agg_exprs)
9968                    .map_err(|e| runtime_err(format!("{e}")))?;
9969                Ok(VmValue::Table(VmTable { df: aggregated }))
9970            }
9971            "join" => {
9972                if ast_args.is_empty() {
9973                    return Err(runtime_err(
9974                        "join() expects at least 1 argument (right table)",
9975                    ));
9976                }
9977                // Evaluate first arg to get right table
9978                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
9979                let right_df = match right_table {
9980                    VmValue::Table(t) => t.df,
9981                    _ => return Err(runtime_err("join() first arg must be a table")),
9982                };
9983                let mut left_cols: Vec<String> = Vec::new();
9984                let mut right_cols: Vec<String> = Vec::new();
9985                let mut join_type = JoinType::Inner;
9986                for arg in &ast_args[1..] {
9987                    match arg {
9988                        AstExpr::NamedArg { name, value } if name == "on" => {
9989                            if let AstExpr::BinOp {
9990                                left,
9991                                op: tl_ast::BinOp::Eq,
9992                                right,
9993                            } = value.as_ref()
9994                            {
9995                                let lc = match left.as_ref() {
9996                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9997                                    _ => {
9998                                        return Err(runtime_err(
9999                                            "on: left side must be a column name",
10000                                        ));
10001                                    }
10002                                };
10003                                let rc = match right.as_ref() {
10004                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10005                                    _ => {
10006                                        return Err(runtime_err(
10007                                            "on: right side must be a column name",
10008                                        ));
10009                                    }
10010                                };
10011                                left_cols.push(lc);
10012                                right_cols.push(rc);
10013                            }
10014                        }
10015                        AstExpr::NamedArg { name, value } if name == "kind" => {
10016                            if let AstExpr::String(kind_str) = value.as_ref() {
10017                                join_type = match kind_str.as_str() {
10018                                    "inner" => JoinType::Inner,
10019                                    "left" => JoinType::Left,
10020                                    "right" => JoinType::Right,
10021                                    "full" => JoinType::Full,
10022                                    _ => {
10023                                        return Err(runtime_err(format!(
10024                                            "Unknown join type: {kind_str}"
10025                                        )));
10026                                    }
10027                                };
10028                            }
10029                        }
10030                        _ => {}
10031                    }
10032                }
10033                let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
10034                let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
10035                let joined = df
10036                    .join(right_df, join_type, &lc_refs, &rc_refs, None)
10037                    .map_err(|e| runtime_err(format!("{e}")))?;
10038                Ok(VmValue::Table(VmTable { df: joined }))
10039            }
10040            "head" | "limit" => {
10041                let n = match ast_args.first() {
10042                    Some(AstExpr::Int(n)) => *n as usize,
10043                    None => 10,
10044                    _ => return Err(runtime_err("head/limit expects an integer")),
10045                };
10046                let limited = df
10047                    .limit(0, Some(n))
10048                    .map_err(|e| runtime_err(format!("{e}")))?;
10049                Ok(VmValue::Table(VmTable { df: limited }))
10050            }
10051            "collect" => {
10052                let batches = self.engine().collect(df).map_err(runtime_err)?;
10053                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10054                Ok(VmValue::String(Arc::from(formatted.as_str())))
10055            }
10056            "show" => {
10057                let limit = match ast_args.first() {
10058                    Some(AstExpr::Int(n)) => *n as usize,
10059                    None => 20,
10060                    _ => 20,
10061                };
10062                let limited = df
10063                    .limit(0, Some(limit))
10064                    .map_err(|e| runtime_err(format!("{e}")))?;
10065                let batches = self.engine().collect(limited).map_err(runtime_err)?;
10066                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10067                println!("{formatted}");
10068                self.output.push(formatted);
10069                Ok(VmValue::None)
10070            }
10071            "describe" => {
10072                let schema = df.schema();
10073                let mut lines = Vec::new();
10074                lines.push("Columns:".to_string());
10075                for field in schema.fields() {
10076                    lines.push(format!("  {}: {}", field.name(), field.data_type()));
10077                }
10078                let output = lines.join("\n");
10079                println!("{output}");
10080                self.output.push(output.clone());
10081                Ok(VmValue::String(Arc::from(output.as_str())))
10082            }
10083            "write_csv" => {
10084                if ast_args.len() != 1 {
10085                    return Err(runtime_err("write_csv() expects 1 argument (path)"));
10086                }
10087                let path = self.eval_ast_to_string(&ast_args[0])?;
10088                self.engine().write_csv(df, &path).map_err(runtime_err)?;
10089                Ok(VmValue::None)
10090            }
10091            "write_parquet" => {
10092                if ast_args.len() != 1 {
10093                    return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10094                }
10095                let path = self.eval_ast_to_string(&ast_args[0])?;
10096                self.engine()
10097                    .write_parquet(df, &path)
10098                    .map_err(runtime_err)?;
10099                Ok(VmValue::None)
10100            }
10101            // Phase 15: Data quality pipe operations
10102            "fill_null" => {
10103                if ast_args.is_empty() {
10104                    return Err(runtime_err(
10105                        "fill_null() expects (column, [strategy/value])",
10106                    ));
10107                }
10108                let column = self.eval_ast_to_string(&ast_args[0])?;
10109                if ast_args.len() >= 2 {
10110                    let val = self.eval_ast_to_vm(&ast_args[1])?;
10111                    match val {
10112                        VmValue::String(s) => {
10113                            // String means strategy name
10114                            let fill_val = if ast_args.len() >= 3 {
10115                                match self.eval_ast_to_vm(&ast_args[2])? {
10116                                    VmValue::Int(n) => Some(n as f64),
10117                                    VmValue::Float(f) => Some(f),
10118                                    _ => None,
10119                                }
10120                            } else {
10121                                None
10122                            };
10123                            let result = self
10124                                .engine()
10125                                .fill_null(df, &column, &s, fill_val)
10126                                .map_err(runtime_err)?;
10127                            Ok(VmValue::Table(VmTable { df: result }))
10128                        }
10129                        VmValue::Int(n) => {
10130                            let result = self
10131                                .engine()
10132                                .fill_null(df, &column, "value", Some(n as f64))
10133                                .map_err(runtime_err)?;
10134                            Ok(VmValue::Table(VmTable { df: result }))
10135                        }
10136                        VmValue::Float(f) => {
10137                            let result = self
10138                                .engine()
10139                                .fill_null(df, &column, "value", Some(f))
10140                                .map_err(runtime_err)?;
10141                            Ok(VmValue::Table(VmTable { df: result }))
10142                        }
10143                        _ => Err(runtime_err(
10144                            "fill_null() second arg must be a strategy or fill value",
10145                        )),
10146                    }
10147                } else {
10148                    let result = self
10149                        .engine()
10150                        .fill_null(df, &column, "zero", None)
10151                        .map_err(runtime_err)?;
10152                    Ok(VmValue::Table(VmTable { df: result }))
10153                }
10154            }
10155            "drop_null" => {
10156                if ast_args.is_empty() {
10157                    return Err(runtime_err("drop_null() expects (column)"));
10158                }
10159                let column = self.eval_ast_to_string(&ast_args[0])?;
10160                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10161                Ok(VmValue::Table(VmTable { df: result }))
10162            }
10163            "dedup" => {
10164                let columns: Vec<String> = ast_args
10165                    .iter()
10166                    .filter_map(|a| self.eval_ast_to_string(a).ok())
10167                    .collect();
10168                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10169                Ok(VmValue::Table(VmTable { df: result }))
10170            }
10171            "clamp" => {
10172                if ast_args.len() < 3 {
10173                    return Err(runtime_err("clamp() expects (column, min, max)"));
10174                }
10175                let column = self.eval_ast_to_string(&ast_args[0])?;
10176                let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10177                    VmValue::Int(n) => n as f64,
10178                    VmValue::Float(f) => f,
10179                    _ => return Err(runtime_err("clamp() min must be a number")),
10180                };
10181                let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10182                    VmValue::Int(n) => n as f64,
10183                    VmValue::Float(f) => f,
10184                    _ => return Err(runtime_err("clamp() max must be a number")),
10185                };
10186                let result = self
10187                    .engine()
10188                    .clamp(df, &column, min_val, max_val)
10189                    .map_err(runtime_err)?;
10190                Ok(VmValue::Table(VmTable { df: result }))
10191            }
10192            "data_profile" => {
10193                let result = self.engine().data_profile(df).map_err(runtime_err)?;
10194                Ok(VmValue::Table(VmTable { df: result }))
10195            }
10196            "row_count" => {
10197                let count = self.engine().row_count(df).map_err(runtime_err)?;
10198                Ok(VmValue::Int(count))
10199            }
10200            "null_rate" => {
10201                if ast_args.is_empty() {
10202                    return Err(runtime_err("null_rate() expects (column)"));
10203                }
10204                let column = self.eval_ast_to_string(&ast_args[0])?;
10205                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10206                Ok(VmValue::Float(rate))
10207            }
10208            "is_unique" => {
10209                if ast_args.is_empty() {
10210                    return Err(runtime_err("is_unique() expects (column)"));
10211                }
10212                let column = self.eval_ast_to_string(&ast_args[0])?;
10213                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10214                Ok(VmValue::Bool(unique))
10215            }
10216            // Phase F2: Window functions
10217            "window" => {
10218                use tl_data::datafusion::logical_expr::{
10219                    WindowFrame, WindowFunctionDefinition,
10220                    expr::{Sort as DfSort, WindowFunction as WinFunc},
10221                };
10222                if ast_args.is_empty() {
10223                    return Err(runtime_err(
10224                        "window() expects named arguments: fn, partition_by, order_by, alias",
10225                    ));
10226                }
10227                let mut win_fn_name = String::new();
10228                let mut partition_by_cols: Vec<String> = Vec::new();
10229                let mut order_by_cols: Vec<String> = Vec::new();
10230                let mut alias_name = String::new();
10231                let mut win_args: Vec<String> = Vec::new();
10232                let mut descending = false;
10233
10234                for arg in &ast_args {
10235                    if let AstExpr::NamedArg { name, value } = arg {
10236                        match name.as_str() {
10237                            "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10238                            "partition_by" => match value.as_ref() {
10239                                AstExpr::List(items) => {
10240                                    for item in items {
10241                                        partition_by_cols.push(self.eval_ast_to_string(item)?);
10242                                    }
10243                                }
10244                                _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10245                            },
10246                            "order_by" => match value.as_ref() {
10247                                AstExpr::List(items) => {
10248                                    for item in items {
10249                                        order_by_cols.push(self.eval_ast_to_string(item)?);
10250                                    }
10251                                }
10252                                _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10253                            },
10254                            "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10255                            "args" => match value.as_ref() {
10256                                AstExpr::List(items) => {
10257                                    for item in items {
10258                                        win_args.push(self.eval_ast_to_string(item)?);
10259                                    }
10260                                }
10261                                _ => win_args.push(self.eval_ast_to_string(value)?),
10262                            },
10263                            "desc" => {
10264                                if let AstExpr::Bool(b) = value.as_ref() {
10265                                    descending = *b;
10266                                }
10267                            }
10268                            _ => {}
10269                        }
10270                    }
10271                }
10272
10273                if win_fn_name.is_empty() {
10274                    return Err(runtime_err(
10275                        "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10276                    ));
10277                }
10278                if alias_name.is_empty() {
10279                    alias_name = win_fn_name.clone();
10280                }
10281
10282                // Build window function definition
10283                let session = self.engine().session_ctx();
10284                let win_udf = match win_fn_name.as_str() {
10285                    "rank" => session.udwf("rank"),
10286                    "dense_rank" => session.udwf("dense_rank"),
10287                    "row_number" => session.udwf("row_number"),
10288                    "percent_rank" => session.udwf("percent_rank"),
10289                    "cume_dist" => session.udwf("cume_dist"),
10290                    "ntile" => session.udwf("ntile"),
10291                    "lag" => session.udwf("lag"),
10292                    "lead" => session.udwf("lead"),
10293                    "first_value" => session.udwf("first_value"),
10294                    "last_value" => session.udwf("last_value"),
10295                    _ => {
10296                        return Err(runtime_err(format!(
10297                            "Unknown window function: {win_fn_name}"
10298                        )));
10299                    }
10300                }
10301                .map_err(|e| {
10302                    runtime_err(format!(
10303                        "Window function '{win_fn_name}' not available: {e}"
10304                    ))
10305                })?;
10306
10307                let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10308
10309                // Build function args (for lag/lead/ntile)
10310                let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10311                    .iter()
10312                    .map(|a| {
10313                        if let Ok(n) = a.parse::<i64>() {
10314                            lit(n)
10315                        } else {
10316                            col(a.as_str())
10317                        }
10318                    })
10319                    .collect();
10320
10321                let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10322                    partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10323                let order_exprs: Vec<DfSort> = order_by_cols
10324                    .iter()
10325                    .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10326                    .collect();
10327
10328                let has_order = !order_exprs.is_empty();
10329                let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10330                    fun,
10331                    args: func_args,
10332                    partition_by: partition_exprs,
10333                    order_by: order_exprs,
10334                    window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10335                    null_treatment: None,
10336                })
10337                .alias(&alias_name);
10338
10339                // Get all existing columns and add the window column
10340                let schema = df.schema();
10341                let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10342                    .fields()
10343                    .iter()
10344                    .map(|f| col(f.name().as_str()))
10345                    .collect();
10346                select_exprs.push(win_expr);
10347
10348                let result_df = df
10349                    .select(select_exprs)
10350                    .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10351                Ok(VmValue::Table(VmTable { df: result_df }))
10352            }
10353            // Phase F3: Union
10354            "union" => {
10355                if ast_args.is_empty() {
10356                    return Err(runtime_err("union() expects a table argument"));
10357                }
10358                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10359                let right_df = match right_table {
10360                    VmValue::Table(t) => t.df,
10361                    _ => return Err(runtime_err("union() argument must be a table")),
10362                };
10363                let result_df = df
10364                    .union(right_df)
10365                    .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10366                Ok(VmValue::Table(VmTable { df: result_df }))
10367            }
10368            // Phase F4: Table sampling
10369            "sample" => {
10370                use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10371                use tl_data::datafusion::datasource::MemTable;
10372                if ast_args.is_empty() {
10373                    return Err(runtime_err("sample() expects a count or fraction"));
10374                }
10375                let batches = self.engine().collect(df).map_err(runtime_err)?;
10376                let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10377                let sample_count = match &ast_args[0] {
10378                    AstExpr::Int(n) => (*n as usize).min(total_rows),
10379                    AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10380                        ((total_rows as f64) * f).ceil() as usize
10381                    }
10382                    _ => {
10383                        let val = self.eval_ast_to_string(&ast_args[0])?;
10384                        val.parse::<usize>().map_err(|_| {
10385                            runtime_err("sample() expects integer count or float fraction")
10386                        })?
10387                    }
10388                };
10389                if total_rows == 0 || sample_count == 0 {
10390                    let schema = batches[0].schema();
10391                    let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10392                        schema.clone(),
10393                    );
10394                    let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10395                        .map_err(|e| runtime_err(format!("{e}")))?;
10396                    let new_df = self
10397                        .engine()
10398                        .session_ctx()
10399                        .read_table(Arc::new(mem_table))
10400                        .map_err(|e| runtime_err(format!("{e}")))?;
10401                    return Ok(VmValue::Table(VmTable { df: new_df }));
10402                }
10403                // Random sampling
10404                let mut rng = rand::thread_rng();
10405                let mut indices: Vec<usize> = (0..total_rows).collect();
10406                use rand::seq::SliceRandom;
10407                indices.partial_shuffle(&mut rng, sample_count);
10408                indices.truncate(sample_count);
10409                indices.sort();
10410                // Concatenate and take
10411                let combined = compute::concat_batches(&batches[0].schema(), &batches)
10412                    .map_err(|e| runtime_err(format!("{e}")))?;
10413                let idx_array =
10414                    UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10415                let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10416                    .num_columns())
10417                    .map(|c| {
10418                        compute::take(combined.column(c), &idx_array, None)
10419                            .map_err(|e| runtime_err(format!("{e}")))
10420                    })
10421                    .collect::<Result<Vec<_>, _>>()?;
10422                let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10423                    combined.schema(),
10424                    sampled_cols,
10425                )
10426                .map_err(|e| runtime_err(format!("{e}")))?;
10427                let mem_table =
10428                    MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10429                        .map_err(|e| runtime_err(format!("{e}")))?;
10430                let new_df = self
10431                    .engine()
10432                    .session_ctx()
10433                    .read_table(Arc::new(mem_table))
10434                    .map_err(|e| runtime_err(format!("{e}")))?;
10435                Ok(VmValue::Table(VmTable { df: new_df }))
10436            }
10437            _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10438        }
10439    }
10440
10441    /// Fallback for table pipe when left side is not a table.
10442    /// Converts to a regular function/builtin call with left as first arg.
10443    fn table_pipe_fallback(
10444        &mut self,
10445        left_val: VmValue,
10446        frame_idx: usize,
10447        op_const: u8,
10448        args_const: u8,
10449    ) -> Result<VmValue, TlError> {
10450        let frame = &self.frames[frame_idx];
10451        let op_name = match &frame.prototype.constants[op_const as usize] {
10452            Constant::String(s) => s.to_string(),
10453            _ => return Err(runtime_err("Expected string constant for table op")),
10454        };
10455        let ast_args = match &frame.prototype.constants[args_const as usize] {
10456            Constant::AstExprList(args) => args.clone(),
10457            _ => return Err(runtime_err("Expected AST expr list for table args")),
10458        };
10459
10460        // Try as builtin with left as first arg
10461        if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10462            // Evaluate AST args to VM values
10463            let mut all_args = vec![left_val];
10464            for arg in &ast_args {
10465                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10466            }
10467            let args_base = self.stack.len();
10468            for arg in &all_args {
10469                self.stack.push(arg.clone());
10470            }
10471            let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10472            self.stack.truncate(args_base);
10473            return result;
10474        }
10475
10476        // Try as user-defined function
10477        if let Some(func) = self.globals.get(&op_name).cloned() {
10478            let mut all_args = vec![left_val];
10479            for arg in &ast_args {
10480                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10481            }
10482            return self.call_vm_function(&func, &all_args);
10483        }
10484
10485        Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10486    }
10487
10488    /// Build TranslateContext from VM globals and stack.
10489    #[cfg(feature = "native")]
10490    fn build_translate_context(&self) -> TranslateContext {
10491        let mut ctx = TranslateContext::new();
10492        // Add globals
10493        for (name, val) in &self.globals {
10494            let local = match val {
10495                VmValue::Int(n) => Some(LocalValue::Int(*n)),
10496                VmValue::Float(f) => Some(LocalValue::Float(*f)),
10497                VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10498                VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10499                _ => None,
10500            };
10501            if let Some(l) = local {
10502                ctx.locals.insert(name.clone(), l);
10503            }
10504        }
10505        // Add locals from current frame
10506        if let Some(frame) = self.frames.last() {
10507            for local_idx in 0..frame.prototype.num_locals as usize {
10508                if let Some(val) = self.stack.get(frame.base + local_idx) {
10509                    // We'd need local name info — for now, rely on globals
10510                    let _ = val;
10511                }
10512            }
10513        }
10514        ctx
10515    }
10516
10517    /// Evaluate an AST expression to a VmValue.
10518    /// For simple expressions does direct lookup; for complex ones, compiles and runs.
10519    fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
10520        match expr {
10521            AstExpr::Ident(name) => {
10522                // Look up in globals first
10523                if let Some(val) = self.globals.get(name) {
10524                    return Ok(val.clone());
10525                }
10526                // Check current frame's stack
10527                if let Some(frame) = self.frames.last() {
10528                    for i in 0..frame.prototype.num_registers as usize {
10529                        if let Some(val) = self.stack.get(frame.base + i)
10530                            && !matches!(val, VmValue::None)
10531                        {
10532                            // Without name->register mapping, we can't be sure
10533                            // which register holds this variable
10534                        }
10535                    }
10536                }
10537                Err(runtime_err(format!("Undefined variable: `{name}`")))
10538            }
10539            AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
10540            AstExpr::Int(n) => Ok(VmValue::Int(*n)),
10541            AstExpr::Float(f) => Ok(VmValue::Float(*f)),
10542            AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
10543            AstExpr::None => Ok(VmValue::None),
10544            AstExpr::Closure {
10545                params: _, body: _, ..
10546            } => {
10547                use crate::compiler;
10548                let wrapper = tl_ast::Program {
10549                    statements: vec![tl_ast::Stmt {
10550                        kind: tl_ast::StmtKind::Expr(expr.clone()),
10551                        span: tl_errors::Span::new(0, 0),
10552                        doc_comment: None,
10553                    }],
10554                    module_doc: None,
10555                };
10556                let proto = compiler::compile(&wrapper)?;
10557                let mut temp_vm = Vm::new();
10558                // Copy globals
10559                temp_vm.globals = self.globals.clone();
10560                let result = temp_vm.execute(&proto)?;
10561                Ok(result)
10562            }
10563            _ => {
10564                // For complex expressions, compile and evaluate
10565                let wrapper = tl_ast::Program {
10566                    statements: vec![tl_ast::Stmt {
10567                        kind: tl_ast::StmtKind::Expr(expr.clone()),
10568                        span: tl_errors::Span::new(0, 0),
10569                        doc_comment: None,
10570                    }],
10571                    module_doc: None,
10572                };
10573                use crate::compiler;
10574                let proto = compiler::compile(&wrapper)?;
10575                let mut temp_vm = Vm::new();
10576                temp_vm.globals = self.globals.clone();
10577                temp_vm.execute(&proto)
10578            }
10579        }
10580    }
10581
10582    fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
10583        match self.eval_ast_to_vm(expr)? {
10584            VmValue::String(s) => Ok(s.to_string()),
10585            _ => Err(runtime_err("Expected a string")),
10586        }
10587    }
10588
10589    /// Simple string interpolation.
10590    fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
10591        let mut result = String::new();
10592        let mut chars = s.chars().peekable();
10593        while let Some(ch) = chars.next() {
10594            if ch == '{' {
10595                let mut var_name = String::new();
10596                let mut depth = 1;
10597                for c in chars.by_ref() {
10598                    if c == '{' {
10599                        depth += 1;
10600                    } else if c == '}' {
10601                        depth -= 1;
10602                        if depth == 0 {
10603                            break;
10604                        }
10605                    }
10606                    var_name.push(c);
10607                }
10608                // Look up variable
10609                if let Some(val) = self.globals.get(&var_name) {
10610                    result.push_str(&format!("{val}"));
10611                } else {
10612                    // Check locals in current frame
10613                    // For now, fall back to globals only — local name info
10614                    // would need debug symbols from the compiler
10615                    result.push('{');
10616                    result.push_str(&var_name);
10617                    result.push('}');
10618                }
10619            } else if ch == '\\' {
10620                match chars.next() {
10621                    Some('n') => result.push('\n'),
10622                    Some('t') => result.push('\t'),
10623                    Some('\\') => result.push('\\'),
10624                    Some('"') => result.push('"'),
10625                    Some(c) => {
10626                        result.push('\\');
10627                        result.push(c);
10628                    }
10629                    None => result.push('\\'),
10630                }
10631            } else {
10632                result.push(ch);
10633            }
10634        }
10635        Ok(result)
10636    }
10637
10638    /// Execute a single bytecode instruction at the given base offset.
10639    /// Used by the LLVM backend's Tier 3 fallback to run complex opcodes on the VM.
10640    pub fn execute_single_instruction(
10641        &mut self,
10642        inst: u32,
10643        proto: &Prototype,
10644        base: usize,
10645    ) -> Result<Option<VmValue>, TlError> {
10646        use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
10647
10648        let proto = Arc::new(proto.clone());
10649        // Push a temporary call frame so the VM can resolve constants etc.
10650        self.frames.push(CallFrame {
10651            prototype: proto.clone(),
10652            ip: 0,
10653            base,
10654            upvalues: Vec::new(),
10655        });
10656        let frame_idx = self.frames.len() - 1;
10657
10658        let op = decode_op(inst);
10659        let a = decode_a(inst);
10660        let _b = decode_b(inst);
10661        let _c = decode_c(inst);
10662        let bx = decode_bx(inst);
10663
10664        // Dispatch the single opcode. We handle the most common
10665        // Tier 3 ops here; anything not handled returns Ok(None).
10666        let result = match op {
10667            Op::GetGlobal => {
10668                let name = self.get_string_constant(frame_idx, bx)?;
10669                let val = self
10670                    .globals
10671                    .get(name.as_ref())
10672                    .cloned()
10673                    .unwrap_or(VmValue::None);
10674                self.stack[base + a as usize] = val;
10675                Ok(None)
10676            }
10677            Op::SetGlobal => {
10678                let name = self.get_string_constant(frame_idx, bx)?;
10679                let val = self.stack[base + a as usize].clone();
10680                self.globals.insert(name.to_string(), val);
10681                Ok(None)
10682            }
10683            _ => {
10684                // For opcodes not explicitly handled, return Ok — the caller
10685                // should have handled Tier 1/2 in LLVM IR.
10686                Ok(None)
10687            }
10688        };
10689
10690        self.frames.pop();
10691        result
10692    }
10693}
10694
10695impl Default for Vm {
10696    fn default() -> Self {
10697        Self::new()
10698    }
10699}
10700
10701#[cfg(test)]
10702mod tests {
10703    use super::*;
10704    use crate::compiler::compile;
10705    use tl_parser::parse;
10706
10707    fn run(source: &str) -> Result<VmValue, TlError> {
10708        let program = parse(source)?;
10709        let proto = compile(&program)?;
10710        let mut vm = Vm::new();
10711        vm.execute(&proto)
10712    }
10713
10714    fn run_output(source: &str) -> Vec<String> {
10715        let program = parse(source).unwrap();
10716        let proto = compile(&program).unwrap();
10717        let mut vm = Vm::new();
10718        vm.execute(&proto).unwrap();
10719        vm.output
10720    }
10721
10722    #[test]
10723    fn test_vm_arithmetic() {
10724        assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
10725        assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
10726        assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
10727        assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
10728        assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
10729        assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
10730        let output = run_output("print(1 + 2)");
10731        assert_eq!(output, vec!["3"]);
10732    }
10733
10734    #[test]
10735    fn test_vm_let_and_print() {
10736        let output = run_output("let x = 42\nprint(x)");
10737        assert_eq!(output, vec!["42"]);
10738    }
10739
10740    #[test]
10741    fn test_vm_function() {
10742        let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
10743        assert_eq!(output, vec!["42"]);
10744    }
10745
10746    #[test]
10747    fn test_vm_if_else() {
10748        let output =
10749            run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
10750        assert_eq!(output, vec!["big"]);
10751    }
10752
10753    #[test]
10754    fn test_vm_list() {
10755        let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
10756        assert_eq!(output, vec!["3"]);
10757    }
10758
10759    #[test]
10760    fn test_vm_map_builtin() {
10761        let output = run_output(
10762            "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
10763        );
10764        assert_eq!(output, vec!["[2, 4, 6]"]);
10765    }
10766
10767    #[test]
10768    fn test_vm_filter_builtin() {
10769        let output = run_output(
10770            "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
10771        );
10772        assert_eq!(output, vec!["[2, 4]"]);
10773    }
10774
10775    #[test]
10776    fn test_vm_for_loop() {
10777        let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
10778        assert_eq!(output, vec!["10"]);
10779    }
10780
10781    #[test]
10782    fn test_vm_closure() {
10783        let output = run_output("let double = (x) => x * 2\nprint(double(5))");
10784        assert_eq!(output, vec!["10"]);
10785    }
10786
10787    #[test]
10788    fn test_vm_sum() {
10789        let output = run_output("print(sum([1, 2, 3, 4]))");
10790        assert_eq!(output, vec!["10"]);
10791    }
10792
10793    #[test]
10794    fn test_vm_reduce() {
10795        let output = run_output(
10796            "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
10797        );
10798        assert_eq!(output, vec!["24"]);
10799    }
10800
10801    #[test]
10802    fn test_vm_pipe() {
10803        let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
10804        assert_eq!(output, vec!["[11, 12, 13]"]);
10805    }
10806
10807    #[test]
10808    fn test_vm_comparison() {
10809        let output = run_output("print(5 > 3)");
10810        assert_eq!(output, vec!["true"]);
10811    }
10812
10813    #[test]
10814    fn test_vm_precedence() {
10815        let output = run_output("print(2 + 3 * 4)");
10816        assert_eq!(output, vec!["14"]);
10817    }
10818
10819    #[test]
10820    fn test_vm_match() {
10821        let output =
10822            run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
10823        assert_eq!(output, vec!["two"]);
10824    }
10825
10826    #[test]
10827    fn test_vm_match_wildcard() {
10828        let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
10829        assert_eq!(output, vec!["other"]);
10830    }
10831
10832    #[test]
10833    fn test_vm_match_binding() {
10834        let output = run_output("print(match 42 { val => val + 1 })");
10835        assert_eq!(output, vec!["43"]);
10836    }
10837
10838    #[test]
10839    fn test_vm_match_guard() {
10840        let output = run_output(
10841            "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10842        );
10843        assert_eq!(output, vec!["pos"]);
10844    }
10845
10846    #[test]
10847    fn test_vm_match_guard_negative() {
10848        let output = run_output(
10849            "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10850        );
10851        assert_eq!(output, vec!["neg"]);
10852    }
10853
10854    #[test]
10855    fn test_vm_match_guard_zero() {
10856        let output = run_output(
10857            "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10858        );
10859        assert_eq!(output, vec!["zero"]);
10860    }
10861
10862    #[test]
10863    fn test_vm_match_enum_destructure() {
10864        let output = run_output(
10865            r#"
10866enum Shape { Circle(int64), Rect(int64, int64) }
10867let s = Shape::Circle(5)
10868print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10869"#,
10870        );
10871        assert_eq!(output, vec!["5"]);
10872    }
10873
10874    #[test]
10875    fn test_vm_match_enum_destructure_rect() {
10876        let output = run_output(
10877            r#"
10878enum Shape { Circle(int64), Rect(int64, int64) }
10879let s = Shape::Rect(3, 4)
10880print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10881"#,
10882        );
10883        assert_eq!(output, vec!["12"]);
10884    }
10885
10886    #[test]
10887    fn test_vm_match_enum_wildcard_field() {
10888        let output = run_output(
10889            r#"
10890enum Pair { Two(int64, int64) }
10891let p = Pair::Two(10, 20)
10892print(match p { Pair::Two(_, y) => y, _ => 0 })
10893"#,
10894        );
10895        assert_eq!(output, vec!["20"]);
10896    }
10897
10898    #[test]
10899    fn test_vm_match_enum_guard() {
10900        let output = run_output(
10901            r#"
10902enum Result { Ok(int64), Err(string) }
10903let r = Result::Ok(150)
10904print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
10905"#,
10906        );
10907        assert_eq!(output, vec!["big"]);
10908    }
10909
10910    #[test]
10911    fn test_vm_match_or_pattern() {
10912        let output =
10913            run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10914        assert_eq!(output, vec!["small"]);
10915    }
10916
10917    #[test]
10918    fn test_vm_match_or_pattern_no_match() {
10919        let output =
10920            run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10921        assert_eq!(output, vec!["big"]);
10922    }
10923
10924    #[test]
10925    fn test_vm_match_string() {
10926        let output = run_output(
10927            r#"let s = "hello"
10928print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
10929        );
10930        assert_eq!(output, vec!["2"]);
10931    }
10932
10933    #[test]
10934    fn test_vm_match_bool() {
10935        let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
10936        assert_eq!(output, vec!["yes"]);
10937    }
10938
10939    #[test]
10940    fn test_vm_match_none() {
10941        let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
10942        assert_eq!(output, vec!["nothing"]);
10943    }
10944
10945    #[test]
10946    fn test_vm_let_destructure_list() {
10947        let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
10948        assert_eq!(output, vec!["1", "2", "3"]);
10949    }
10950
10951    #[test]
10952    fn test_vm_let_destructure_list_rest() {
10953        let output =
10954            run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
10955        assert_eq!(output, vec!["1", "3"]);
10956    }
10957
10958    #[test]
10959    fn test_vm_let_destructure_struct() {
10960        let output = run_output(
10961            r#"
10962struct Point { x: int64, y: int64 }
10963let p = Point { x: 10, y: 20 }
10964let Point { x, y } = p
10965print(x)
10966print(y)
10967"#,
10968        );
10969        assert_eq!(output, vec!["10", "20"]);
10970    }
10971
10972    #[test]
10973    fn test_vm_let_destructure_struct_anon() {
10974        let output = run_output(
10975            r#"
10976struct Point { x: int64, y: int64 }
10977let p = Point { x: 10, y: 20 }
10978let { x, y } = p
10979print(x)
10980print(y)
10981"#,
10982        );
10983        assert_eq!(output, vec!["10", "20"]);
10984    }
10985
10986    #[test]
10987    fn test_vm_match_struct_pattern() {
10988        let output = run_output(
10989            r#"
10990struct Point { x: int64, y: int64 }
10991let p = Point { x: 1, y: 2 }
10992print(match p { Point { x, y } => x + y, _ => 0 })
10993"#,
10994        );
10995        assert_eq!(output, vec!["3"]);
10996    }
10997
10998    #[test]
10999    fn test_vm_match_list_pattern() {
11000        let output = run_output(
11001            r#"
11002let lst = [1, 2, 3]
11003print(match lst { [a, b, c] => a + b + c, _ => 0 })
11004"#,
11005        );
11006        assert_eq!(output, vec!["6"]);
11007    }
11008
11009    #[test]
11010    fn test_vm_match_list_rest_pattern() {
11011        let output = run_output(
11012            r#"
11013let lst = [10, 20, 30, 40]
11014print(match lst { [head, ...rest] => head, _ => 0 })
11015"#,
11016        );
11017        assert_eq!(output, vec!["10"]);
11018    }
11019
11020    #[test]
11021    fn test_vm_match_list_empty() {
11022        let output = run_output(
11023            r#"
11024let lst = []
11025print(match lst { [] => "empty", _ => "nonempty" })
11026"#,
11027        );
11028        assert_eq!(output, vec!["empty"]);
11029    }
11030
11031    #[test]
11032    fn test_vm_match_list_length_mismatch() {
11033        let output = run_output(
11034            r#"
11035let lst = [1, 2, 3]
11036print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
11037"#,
11038        );
11039        assert_eq!(output, vec!["three"]);
11040    }
11041
11042    #[test]
11043    fn test_vm_match_negative_literal() {
11044        let output =
11045            run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
11046        assert_eq!(output, vec!["neg one"]);
11047    }
11048
11049    #[test]
11050    fn test_vm_case_with_pattern() {
11051        let output = run_output(
11052            r#"
11053let x = 5
11054let result = case {
11055    x > 10 => "big",
11056    x > 0 => "positive",
11057    _ => "other"
11058}
11059print(result)
11060"#,
11061        );
11062        assert_eq!(output, vec!["positive"]);
11063    }
11064
11065    #[test]
11066    fn test_vm_parallel_map() {
11067        // Build a range > PARALLEL_THRESHOLD and map with a pure function
11068        let result = run("map(range(15000), (x) => x * 2)").unwrap();
11069        if let VmValue::List(items) = result {
11070            assert_eq!(items.len(), 15000);
11071            assert!(matches!(items[0], VmValue::Int(0)));
11072            assert!(matches!(items[1], VmValue::Int(2)));
11073            assert!(matches!(items[14999], VmValue::Int(29998)));
11074        } else {
11075            panic!("Expected list, got {:?}", result);
11076        }
11077    }
11078
11079    #[test]
11080    fn test_vm_parallel_filter() {
11081        let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11082        if let VmValue::List(items) = result {
11083            assert_eq!(items.len(), 10000);
11084            assert!(matches!(items[0], VmValue::Int(0)));
11085            assert!(matches!(items[1], VmValue::Int(2)));
11086        } else {
11087            panic!("Expected list, got {:?}", result);
11088        }
11089    }
11090
11091    #[test]
11092    fn test_vm_parallel_sum() {
11093        let result = run("sum(range(20000))").unwrap();
11094        // sum(0..19999) = 19999 * 20000 / 2 = 199990000
11095        assert!(matches!(result, VmValue::Int(199990000)));
11096    }
11097
11098    #[test]
11099    fn test_vm_recursive_fib() {
11100        let output = run_output(
11101            "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11102        );
11103        assert_eq!(output, vec!["55"]);
11104    }
11105
11106    #[test]
11107    fn test_vm_if_else_expr() {
11108        // if-else as the last expression in a function should return a value
11109        let output = run_output(
11110            "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11111        );
11112        assert_eq!(output, vec!["5", "3"]);
11113    }
11114
11115    // ── Phase 5 tests ──
11116
11117    #[test]
11118    fn test_vm_struct_creation() {
11119        let output = run_output(
11120            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11121        );
11122        assert_eq!(output, vec!["1.0", "2.0"]);
11123    }
11124
11125    #[test]
11126    fn test_vm_struct_nested() {
11127        let output = run_output(
11128            "struct Point { x: float64, y: float64 }\nstruct Line { start: Point, end_pt: Point }\nlet l = Line { start: Point { x: 0.0, y: 0.0 }, end_pt: Point { x: 1.0, y: 1.0 } }\nprint(l.start.x)",
11129        );
11130        assert_eq!(output, vec!["0.0"]);
11131    }
11132
11133    #[test]
11134    fn test_vm_enum_creation() {
11135        let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11136        assert_eq!(output, vec!["Color::Red"]);
11137    }
11138
11139    #[test]
11140    fn test_vm_enum_with_fields() {
11141        let output = run_output(
11142            "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11143        );
11144        assert!(output[0].contains("Circle"));
11145    }
11146
11147    #[test]
11148    fn test_vm_impl_method() {
11149        let output = run_output(
11150            "struct Counter { value: int64 }\nimpl Counter {\n    fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11151        );
11152        assert_eq!(output, vec!["42"]);
11153    }
11154
11155    #[test]
11156    fn test_vm_try_catch_throw() {
11157        let output = run_output("try {\n    throw \"oops\"\n} catch e {\n    print(e)\n}");
11158        assert_eq!(output, vec!["oops"]);
11159    }
11160
11161    #[test]
11162    fn test_vm_string_split() {
11163        let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11164        assert_eq!(output, vec!["[hello, world]"]);
11165    }
11166
11167    #[test]
11168    fn test_vm_string_trim() {
11169        let output = run_output("print(\"  hello  \".trim())");
11170        assert_eq!(output, vec!["hello"]);
11171    }
11172
11173    #[test]
11174    fn test_vm_string_contains() {
11175        let output = run_output("print(\"hello world\".contains(\"world\"))");
11176        assert_eq!(output, vec!["true"]);
11177    }
11178
11179    #[test]
11180    fn test_vm_string_upper_lower() {
11181        let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11182        assert_eq!(output, vec!["HELLO", "hello"]);
11183    }
11184
11185    #[test]
11186    fn test_vm_math_sqrt() {
11187        let output = run_output("print(sqrt(16.0))");
11188        assert_eq!(output, vec!["4.0"]);
11189    }
11190
11191    #[test]
11192    fn test_vm_math_floor_ceil() {
11193        let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11194        assert_eq!(output, vec!["3.0", "4.0"]);
11195    }
11196
11197    #[test]
11198    fn test_vm_math_trig() {
11199        let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11200        assert_eq!(output, vec!["0.0", "1.0"]);
11201    }
11202
11203    #[test]
11204    fn test_vm_assert_pass() {
11205        run("assert(true)").unwrap();
11206        run("assert_eq(1 + 1, 2)").unwrap();
11207    }
11208
11209    #[test]
11210    fn test_vm_assert_fail() {
11211        assert!(run("assert(false)").is_err());
11212        assert!(run("assert_eq(1, 2)").is_err());
11213    }
11214
11215    #[test]
11216    fn test_vm_join() {
11217        let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11218        assert_eq!(output, vec!["a, b, c"]);
11219    }
11220
11221    #[test]
11222    fn test_vm_list_method_len() {
11223        let output = run_output("print([1, 2, 3].len())");
11224        assert_eq!(output, vec!["3"]);
11225    }
11226
11227    #[test]
11228    fn test_vm_list_method_map() {
11229        let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11230        assert_eq!(output, vec!["[2, 4, 6]"]);
11231    }
11232
11233    #[test]
11234    fn test_vm_list_method_filter() {
11235        let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11236        assert_eq!(output, vec!["[4, 5]"]);
11237    }
11238
11239    #[test]
11240    fn test_vm_string_replace() {
11241        let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11242        assert_eq!(output, vec!["hello rust"]);
11243    }
11244
11245    #[test]
11246    fn test_vm_string_starts_ends() {
11247        let output = run_output(
11248            "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11249        );
11250        assert_eq!(output, vec!["true", "true"]);
11251    }
11252
11253    #[test]
11254    fn test_vm_math_log() {
11255        let result = run("log(1.0)").unwrap();
11256        if let VmValue::Float(f) = result {
11257            assert!((f - 0.0).abs() < 1e-10);
11258        } else {
11259            panic!("Expected float");
11260        }
11261    }
11262
11263    #[test]
11264    fn test_vm_pow_builtin() {
11265        let output = run_output("print(pow(2.0, 10.0))");
11266        assert_eq!(output, vec!["1024.0"]);
11267    }
11268
11269    #[test]
11270    fn test_vm_round_builtin() {
11271        let output = run_output("print(round(3.5))");
11272        assert_eq!(output, vec!["4.0"]);
11273    }
11274
11275    #[test]
11276    fn test_vm_try_catch_runtime_error() {
11277        let output = run_output("try {\n    let x = 1 / 0\n} catch e {\n    print(e)\n}");
11278        assert_eq!(output, vec!["Division by zero"]);
11279    }
11280
11281    #[test]
11282    fn test_vm_struct_field_access() {
11283        let output = run_output(
11284            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11285        );
11286        assert_eq!(output, vec!["1.5"]);
11287    }
11288
11289    #[test]
11290    fn test_vm_enum_match() {
11291        let output = run_output(
11292            "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11293        );
11294        // match expression compares enum instances
11295        assert!(!output.is_empty());
11296    }
11297
11298    #[test]
11299    fn test_vm_impl_method_with_args() {
11300        let output = run_output(
11301            "struct Rect { w: float64, h: float64 }\nimpl Rect {\n    fn area(self) { self.w * self.h }\n}\nlet r = Rect { w: 3.0, h: 4.0 }\nprint(r.area())",
11302        );
11303        assert_eq!(output, vec!["12.0"]);
11304    }
11305
11306    #[test]
11307    fn test_vm_string_len() {
11308        let output = run_output("print(\"hello\".len())");
11309        assert_eq!(output, vec!["5"]);
11310    }
11311
11312    #[test]
11313    fn test_vm_list_reduce() {
11314        let output = run_output(
11315            "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11316        );
11317        assert_eq!(output, vec!["10"]);
11318    }
11319
11320    #[test]
11321    fn test_vm_nested_try_catch() {
11322        let output = run_output(
11323            "try {\n    try {\n        throw \"inner\"\n    } catch e {\n        print(e)\n        throw \"outer\"\n    }\n} catch e2 {\n    print(e2)\n}",
11324        );
11325        assert_eq!(output, vec!["inner", "outer"]);
11326    }
11327
11328    #[test]
11329    fn test_vm_math_pow() {
11330        let output = run_output("print(pow(2.0, 10.0))");
11331        assert_eq!(output, vec!["1024.0"]);
11332    }
11333
11334    // ── Phase 6: Stdlib & Ecosystem tests ──
11335
11336    #[test]
11337    fn test_vm_json_parse() {
11338        let output = run_output(
11339            r#"let m = map_from("a", 1, "b", "hello")
11340let s = json_stringify(m)
11341let m2 = json_parse(s)
11342print(m2["a"])
11343print(m2["b"])"#,
11344        );
11345        assert_eq!(output, vec!["1", "hello"]);
11346    }
11347
11348    #[test]
11349    fn test_vm_json_stringify() {
11350        let output = run_output(
11351            r#"let m = map_from("x", 1, "y", 2)
11352let s = json_stringify(m)
11353print(s)"#,
11354        );
11355        assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11356    }
11357
11358    #[test]
11359    fn test_vm_map_from_and_access() {
11360        let output = run_output(
11361            r#"let m = map_from("a", 10, "b", 20)
11362print(m["a"])
11363print(m.b)"#,
11364        );
11365        assert_eq!(output, vec!["10", "20"]);
11366    }
11367
11368    #[test]
11369    fn test_vm_map_methods() {
11370        let output = run_output(
11371            r#"let m = map_from("a", 1, "b", 2)
11372print(m.keys())
11373print(m.values())
11374print(m.contains_key("a"))
11375print(m.len())"#,
11376        );
11377        assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11378    }
11379
11380    #[test]
11381    fn test_vm_map_set_index() {
11382        let output = run_output(
11383            r#"let m = map_from("a", 1)
11384m["b"] = 2
11385print(m["b"])"#,
11386        );
11387        assert_eq!(output, vec!["2"]);
11388    }
11389
11390    #[test]
11391    fn test_vm_map_iteration() {
11392        let output = run_output(
11393            r#"let m = map_from("x", 10, "y", 20)
11394for kv in m {
11395    print(kv[0])
11396}"#,
11397        );
11398        assert_eq!(output, vec!["x", "y"]);
11399    }
11400
11401    #[test]
11402    fn test_vm_file_read_write() {
11403        let output = run_output(
11404            r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11405print(read_file("/tmp/tl_vm_test.txt"))
11406print(file_exists("/tmp/tl_vm_test.txt"))"#,
11407        );
11408        assert_eq!(output, vec!["vm hello", "true"]);
11409    }
11410
11411    #[test]
11412    fn test_vm_env_get_set() {
11413        let output = run_output(
11414            r#"env_set("TL_VM_TEST", "abc")
11415print(env_get("TL_VM_TEST"))"#,
11416        );
11417        assert_eq!(output, vec!["abc"]);
11418    }
11419
11420    #[test]
11421    fn test_vm_regex_match() {
11422        let output = run_output(
11423            r#"print(regex_match("\\d+", "abc123"))
11424print(regex_match("^\\d+$", "abc"))"#,
11425        );
11426        assert_eq!(output, vec!["true", "false"]);
11427    }
11428
11429    #[test]
11430    fn test_vm_regex_find() {
11431        let output = run_output(
11432            r#"let m = regex_find("\\d+", "abc123def456")
11433print(len(m))
11434print(m[0])"#,
11435        );
11436        assert_eq!(output, vec!["2", "123"]);
11437    }
11438
11439    #[test]
11440    fn test_vm_regex_replace() {
11441        let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11442        assert_eq!(output, vec!["abcX"]);
11443    }
11444
11445    #[test]
11446    fn test_vm_now() {
11447        // now() returns DateTime which displays as formatted string
11448        let output = run_output("let t = now()\nprint(type_of(t))");
11449        assert_eq!(output, vec!["datetime"]);
11450    }
11451
11452    #[test]
11453    fn test_vm_date_format() {
11454        let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11455        assert_eq!(output, vec!["2024-01-01"]);
11456    }
11457
11458    #[test]
11459    fn test_vm_date_parse() {
11460        let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11461        assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11462    }
11463
11464    #[test]
11465    fn test_vm_string_chars() {
11466        let output = run_output(r#"print(len("hello".chars()))"#);
11467        assert_eq!(output, vec!["5"]);
11468    }
11469
11470    #[test]
11471    fn test_vm_string_repeat() {
11472        let output = run_output(r#"print("ab".repeat(3))"#);
11473        assert_eq!(output, vec!["ababab"]);
11474    }
11475
11476    #[test]
11477    fn test_vm_string_index_of() {
11478        let output = run_output(r#"print("hello world".index_of("world"))"#);
11479        assert_eq!(output, vec!["6"]);
11480    }
11481
11482    #[test]
11483    fn test_vm_string_substring() {
11484        let output = run_output(r#"print("hello world".substring(0, 5))"#);
11485        assert_eq!(output, vec!["hello"]);
11486    }
11487
11488    #[test]
11489    fn test_vm_string_pad() {
11490        let output = run_output(
11491            r#"print("42".pad_left(5, "0"))
11492print("hi".pad_right(5, "."))"#,
11493        );
11494        assert_eq!(output, vec!["00042", "hi..."]);
11495    }
11496
11497    #[test]
11498    fn test_vm_list_sort() {
11499        let output = run_output(r#"print([3, 1, 2].sort())"#);
11500        assert_eq!(output, vec!["[1, 2, 3]"]);
11501    }
11502
11503    #[test]
11504    fn test_vm_list_reverse() {
11505        let output = run_output(r#"print([1, 2, 3].reverse())"#);
11506        assert_eq!(output, vec!["[3, 2, 1]"]);
11507    }
11508
11509    #[test]
11510    fn test_vm_list_contains() {
11511        let output = run_output(
11512            r#"print([1, 2, 3].contains(2))
11513print([1, 2, 3].contains(5))"#,
11514        );
11515        assert_eq!(output, vec!["true", "false"]);
11516    }
11517
11518    #[test]
11519    fn test_vm_list_slice() {
11520        let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
11521        assert_eq!(output, vec!["[2, 3, 4]"]);
11522    }
11523
11524    #[test]
11525    fn test_vm_zip() {
11526        let output = run_output(
11527            r#"let p = zip([1, 2], ["a", "b"])
11528print(p[0])"#,
11529        );
11530        assert_eq!(output, vec!["[1, a]"]);
11531    }
11532
11533    #[test]
11534    fn test_vm_enumerate() {
11535        let output = run_output(
11536            r#"let e = enumerate(["a", "b", "c"])
11537print(e[1])"#,
11538        );
11539        assert_eq!(output, vec!["[1, b]"]);
11540    }
11541
11542    #[test]
11543    fn test_vm_bool() {
11544        let output = run_output(
11545            r#"print(bool(1))
11546print(bool(0))
11547print(bool(""))"#,
11548        );
11549        assert_eq!(output, vec!["true", "false", "false"]);
11550    }
11551
11552    #[test]
11553    fn test_vm_range_step() {
11554        let output = run_output(r#"print(range(0, 10, 3))"#);
11555        assert_eq!(output, vec!["[0, 3, 6, 9]"]);
11556    }
11557
11558    #[test]
11559    fn test_vm_int_bool() {
11560        let output = run_output(
11561            r#"print(int(true))
11562print(int(false))"#,
11563        );
11564        assert_eq!(output, vec!["1", "0"]);
11565    }
11566
11567    #[test]
11568    fn test_vm_map_len_typeof() {
11569        let output = run_output(
11570            r#"let m = map_from("a", 1)
11571print(len(m))
11572print(type_of(m))"#,
11573        );
11574        assert_eq!(output, vec!["1", "map"]);
11575    }
11576
11577    #[test]
11578    fn test_vm_json_file_roundtrip() {
11579        let output = run_output(
11580            r#"let data = map_from("name", "vm_test", "count", 99)
11581write_file("/tmp/tl_vm_json.json", json_stringify(data))
11582let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
11583print(parsed["name"])
11584print(parsed["count"])"#,
11585        );
11586        assert_eq!(output, vec!["vm_test", "99"]);
11587    }
11588
11589    // ── Phase 7: Concurrency tests ──
11590
11591    #[test]
11592    fn test_vm_spawn_await_basic() {
11593        let output = run_output(
11594            r#"fn worker() { 42 }
11595let t = spawn(worker)
11596let result = await t
11597print(result)"#,
11598        );
11599        assert_eq!(output, vec!["42"]);
11600    }
11601
11602    #[test]
11603    fn test_vm_spawn_closure_with_capture() {
11604        let output = run_output(
11605            r#"let x = 10
11606let f = () => x + 5
11607let t = spawn(f)
11608print(await t)"#,
11609        );
11610        assert_eq!(output, vec!["15"]);
11611    }
11612
11613    #[test]
11614    fn test_vm_sleep() {
11615        let output = run_output(
11616            r#"sleep(10)
11617print("done")"#,
11618        );
11619        assert_eq!(output, vec!["done"]);
11620    }
11621
11622    #[test]
11623    fn test_vm_await_non_task_passthrough() {
11624        let output = run_output(r#"print(await 42)"#);
11625        assert_eq!(output, vec!["42"]);
11626    }
11627
11628    #[test]
11629    fn test_vm_spawn_multiple_await() {
11630        let output = run_output(
11631            r#"fn w1() { 1 }
11632fn w2() { 2 }
11633fn w3() { 3 }
11634let t1 = spawn(w1)
11635let t2 = spawn(w2)
11636let t3 = spawn(w3)
11637let a = await t1
11638let b = await t2
11639let c = await t3
11640print(a + b + c)"#,
11641        );
11642        assert_eq!(output, vec!["6"]);
11643    }
11644
11645    #[test]
11646    fn test_vm_channel_basic() {
11647        let output = run_output(
11648            r#"let ch = channel()
11649send(ch, 42)
11650let val = recv(ch)
11651print(val)"#,
11652        );
11653        assert_eq!(output, vec!["42"]);
11654    }
11655
11656    #[test]
11657    fn test_vm_channel_between_tasks() {
11658        let output = run_output(
11659            r#"let ch = channel()
11660fn producer() { send(ch, 100) }
11661let t = spawn(producer)
11662let val = recv(ch)
11663await t
11664print(val)"#,
11665        );
11666        assert_eq!(output, vec!["100"]);
11667    }
11668
11669    #[test]
11670    fn test_vm_try_recv_empty() {
11671        let output = run_output(
11672            r#"let ch = channel()
11673let val = try_recv(ch)
11674print(val)"#,
11675        );
11676        assert_eq!(output, vec!["none"]);
11677    }
11678
11679    #[test]
11680    fn test_vm_channel_multiple_values() {
11681        let output = run_output(
11682            r#"let ch = channel()
11683send(ch, 1)
11684send(ch, 2)
11685send(ch, 3)
11686print(recv(ch))
11687print(recv(ch))
11688print(recv(ch))"#,
11689        );
11690        assert_eq!(output, vec!["1", "2", "3"]);
11691    }
11692
11693    #[test]
11694    fn test_vm_channel_producer_consumer() {
11695        let output = run_output(
11696            r#"let ch = channel()
11697fn producer() {
11698    send(ch, 10)
11699    send(ch, 20)
11700    send(ch, 30)
11701}
11702let t = spawn(producer)
11703let a = recv(ch)
11704let b = recv(ch)
11705let c = recv(ch)
11706await t
11707print(a + b + c)"#,
11708        );
11709        assert_eq!(output, vec!["60"]);
11710    }
11711
11712    #[test]
11713    fn test_vm_await_all() {
11714        let output = run_output(
11715            r#"fn w1() { 10 }
11716fn w2() { 20 }
11717fn w3() { 30 }
11718let t1 = spawn(w1)
11719let t2 = spawn(w2)
11720let t3 = spawn(w3)
11721let results = await_all([t1, t2, t3])
11722print(sum(results))"#,
11723        );
11724        assert_eq!(output, vec!["60"]);
11725    }
11726
11727    #[test]
11728    fn test_vm_pmap_basic() {
11729        let output = run_output(
11730            r#"let results = pmap([1, 2, 3], (x) => x * 2)
11731print(results)"#,
11732        );
11733        assert_eq!(output, vec!["[2, 4, 6]"]);
11734    }
11735
11736    #[test]
11737    fn test_vm_pmap_order_preserved() {
11738        let output = run_output(
11739            r#"let results = pmap([10, 20, 30], (x) => x + 1)
11740print(results)"#,
11741        );
11742        assert_eq!(output, vec!["[11, 21, 31]"]);
11743    }
11744
11745    #[test]
11746    fn test_vm_timeout_success() {
11747        let output = run_output(
11748            r#"fn worker() { 42 }
11749let t = spawn(worker)
11750let result = timeout(t, 5000)
11751print(result)"#,
11752        );
11753        assert_eq!(output, vec!["42"]);
11754    }
11755
11756    #[test]
11757    fn test_vm_timeout_failure() {
11758        let output = run_output(
11759            r#"fn slow() { sleep(10000) }
11760let t = spawn(slow)
11761let result = "ok"
11762try {
11763    result = timeout(t, 50)
11764} catch e {
11765    result = e
11766}
11767print(result)"#,
11768        );
11769        assert_eq!(output, vec!["Task timed out"]);
11770    }
11771
11772    #[test]
11773    fn test_vm_spawn_error_propagation() {
11774        let output = run_output(
11775            r#"fn bad() { throw "bad thing" }
11776let result = "ok"
11777try {
11778    let t = spawn(bad)
11779    result = await t
11780} catch e {
11781    result = e
11782}
11783print(result)"#,
11784        );
11785        assert_eq!(output, vec!["bad thing"]);
11786    }
11787
11788    #[test]
11789    fn test_vm_spawn_producer_consumer_pipeline() {
11790        let output = run_output(
11791            r#"let ch = channel()
11792fn producer() {
11793    let mut i = 0
11794    while i < 5 {
11795        send(ch, i * 10)
11796        i = i + 1
11797    }
11798}
11799let t = spawn(producer)
11800let mut total = 0
11801let mut count = 0
11802while count < 5 {
11803    total = total + recv(ch)
11804    count = count + 1
11805}
11806await t
11807print(total)"#,
11808        );
11809        assert_eq!(output, vec!["100"]);
11810    }
11811
11812    #[test]
11813    fn test_vm_type_of_task_channel() {
11814        let output = run_output(
11815            r#"fn worker() { 1 }
11816let t = spawn(worker)
11817let ch = channel()
11818print(type_of(t))
11819print(type_of(ch))
11820await t"#,
11821        );
11822        assert_eq!(output, vec!["task", "channel"]);
11823    }
11824
11825    // ── Phase 8: Iterators & Generators ──
11826
11827    #[test]
11828    fn test_vm_basic_generator() {
11829        let output = run_output(
11830            r#"fn gen() {
11831    yield 1
11832    yield 2
11833    yield 3
11834}
11835let g = gen()
11836print(next(g))
11837print(next(g))
11838print(next(g))
11839print(next(g))"#,
11840        );
11841        assert_eq!(output, vec!["1", "2", "3", "none"]);
11842    }
11843
11844    #[test]
11845    fn test_vm_generator_exhaustion() {
11846        let output = run_output(
11847            r#"fn gen() {
11848    yield 42
11849}
11850let g = gen()
11851print(next(g))
11852print(next(g))
11853print(next(g))"#,
11854        );
11855        assert_eq!(output, vec!["42", "none", "none"]);
11856    }
11857
11858    #[test]
11859    fn test_vm_generator_with_loop() {
11860        let output = run_output(
11861            r#"fn counter() {
11862    let mut i = 0
11863    while i < 3 {
11864        yield i
11865        i = i + 1
11866    }
11867}
11868let g = counter()
11869print(next(g))
11870print(next(g))
11871print(next(g))
11872print(next(g))"#,
11873        );
11874        assert_eq!(output, vec!["0", "1", "2", "none"]);
11875    }
11876
11877    #[test]
11878    fn test_vm_generator_with_args() {
11879        let output = run_output(
11880            r#"fn count_from(start) {
11881    let mut i = start
11882    while i < start + 3 {
11883        yield i
11884        i = i + 1
11885    }
11886}
11887let g = count_from(10)
11888print(next(g))
11889print(next(g))
11890print(next(g))
11891print(next(g))"#,
11892        );
11893        assert_eq!(output, vec!["10", "11", "12", "none"]);
11894    }
11895
11896    #[test]
11897    fn test_vm_generator_yield_none() {
11898        let output = run_output(
11899            r#"fn gen() {
11900    yield
11901    yield 5
11902}
11903let g = gen()
11904print(next(g))
11905print(next(g))
11906print(next(g))"#,
11907        );
11908        assert_eq!(output, vec!["none", "5", "none"]);
11909    }
11910
11911    #[test]
11912    fn test_vm_is_generator() {
11913        let output = run_output(
11914            r#"fn gen() { yield 1 }
11915let g = gen()
11916print(is_generator(g))
11917print(is_generator(42))
11918print(is_generator(none))"#,
11919        );
11920        assert_eq!(output, vec!["true", "false", "false"]);
11921    }
11922
11923    #[test]
11924    fn test_vm_multiple_generators() {
11925        let output = run_output(
11926            r#"fn gen() {
11927    yield 1
11928    yield 2
11929}
11930let g1 = gen()
11931let g2 = gen()
11932print(next(g1))
11933print(next(g2))
11934print(next(g1))
11935print(next(g2))"#,
11936        );
11937        assert_eq!(output, vec!["1", "1", "2", "2"]);
11938    }
11939
11940    #[test]
11941    fn test_vm_for_over_generator() {
11942        let output = run_output(
11943            r#"fn gen() {
11944    yield 10
11945    yield 20
11946    yield 30
11947}
11948for x in gen() {
11949    print(x)
11950}"#,
11951        );
11952        assert_eq!(output, vec!["10", "20", "30"]);
11953    }
11954
11955    #[test]
11956    fn test_vm_iter_builtin() {
11957        let output = run_output(
11958            r#"let g = iter([1, 2, 3])
11959print(next(g))
11960print(next(g))
11961print(next(g))
11962print(next(g))"#,
11963        );
11964        assert_eq!(output, vec!["1", "2", "3", "none"]);
11965    }
11966
11967    #[test]
11968    fn test_vm_take_builtin() {
11969        let output = run_output(
11970            r#"fn naturals() {
11971    let mut n = 0
11972    while true {
11973        yield n
11974        n = n + 1
11975    }
11976}
11977let g = take(naturals(), 5)
11978print(next(g))
11979print(next(g))
11980print(next(g))
11981print(next(g))
11982print(next(g))
11983print(next(g))"#,
11984        );
11985        assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
11986    }
11987
11988    #[test]
11989    fn test_vm_skip_builtin() {
11990        let output = run_output(
11991            r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
11992print(next(g))
11993print(next(g))
11994print(next(g))
11995print(next(g))"#,
11996        );
11997        assert_eq!(output, vec!["30", "40", "50", "none"]);
11998    }
11999
12000    #[test]
12001    fn test_vm_gen_collect() {
12002        let output = run_output(
12003            r#"fn gen() {
12004    yield 1
12005    yield 2
12006    yield 3
12007}
12008let result = gen_collect(gen())
12009print(result)"#,
12010        );
12011        assert_eq!(output, vec!["[1, 2, 3]"]);
12012    }
12013
12014    #[test]
12015    fn test_vm_gen_map() {
12016        let output = run_output(
12017            r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
12018print(gen_collect(g))"#,
12019        );
12020        assert_eq!(output, vec!["[10, 20, 30]"]);
12021    }
12022
12023    #[test]
12024    fn test_vm_gen_filter() {
12025        let output = run_output(
12026            r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
12027print(gen_collect(g))"#,
12028        );
12029        assert_eq!(output, vec!["[2, 4, 6]"]);
12030    }
12031
12032    #[test]
12033    fn test_vm_chain() {
12034        let output = run_output(
12035            r#"let g = chain(iter([1, 2]), iter([3, 4]))
12036print(gen_collect(g))"#,
12037        );
12038        assert_eq!(output, vec!["[1, 2, 3, 4]"]);
12039    }
12040
12041    #[test]
12042    fn test_vm_gen_zip() {
12043        let output = run_output(
12044            r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
12045print(gen_collect(g))"#,
12046        );
12047        assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
12048    }
12049
12050    #[test]
12051    fn test_vm_gen_enumerate() {
12052        let output = run_output(
12053            r#"let g = gen_enumerate(iter([10, 20, 30]))
12054print(gen_collect(g))"#,
12055        );
12056        assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12057    }
12058
12059    #[test]
12060    fn test_vm_combinator_chaining() {
12061        let output = run_output(
12062            r#"fn naturals() {
12063    let mut n = 0
12064    while true {
12065        yield n
12066        n = n + 1
12067    }
12068}
12069let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12070print(result)"#,
12071        );
12072        assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12073    }
12074
12075    #[test]
12076    fn test_vm_for_over_take() {
12077        let output = run_output(
12078            r#"fn naturals() {
12079    let mut n = 0
12080    while true {
12081        yield n
12082        n = n + 1
12083    }
12084}
12085for x in take(naturals(), 5) {
12086    print(x)
12087}"#,
12088        );
12089        assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12090    }
12091
12092    #[test]
12093    fn test_vm_generator_error_propagation() {
12094        let result = run(r#"fn bad_gen() {
12095    yield 1
12096    throw "oops"
12097}
12098let g = bad_gen()
12099let mut caught = ""
12100next(g)
12101try {
12102    next(g)
12103} catch e {
12104    caught = e
12105}
12106print(caught)"#);
12107        // Should succeed (error caught)
12108        assert!(result.is_ok());
12109    }
12110
12111    #[test]
12112    fn test_vm_fibonacci_generator() {
12113        let output = run_output(
12114            r#"fn fib() {
12115    let mut a = 0
12116    let mut b = 1
12117    while true {
12118        yield a
12119        let temp = a + b
12120        a = b
12121        b = temp
12122    }
12123}
12124print(gen_collect(take(fib(), 8)))"#,
12125        );
12126        assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12127    }
12128
12129    #[test]
12130    fn test_vm_generator_method_syntax() {
12131        let output = run_output(
12132            r#"fn gen() {
12133    yield 1
12134    yield 2
12135    yield 3
12136}
12137let g = gen()
12138print(type_of(g))"#,
12139        );
12140        assert_eq!(output, vec!["generator"]);
12141    }
12142
12143    // ── Phase 10: Result/Option + ? operator tests ──
12144
12145    #[test]
12146    fn test_vm_ok_err_builtins() {
12147        let output = run_output("let r = Ok(42)\nprint(r)");
12148        assert_eq!(output, vec!["Result::Ok(42)"]);
12149
12150        let output = run_output("let r = Err(\"fail\")\nprint(r)");
12151        assert_eq!(output, vec!["Result::Err(fail)"]);
12152    }
12153
12154    #[test]
12155    fn test_vm_is_ok_is_err() {
12156        let output = run_output("print(is_ok(Ok(42)))");
12157        assert_eq!(output, vec!["true"]);
12158        let output = run_output("print(is_err(Ok(42)))");
12159        assert_eq!(output, vec!["false"]);
12160        let output = run_output("print(is_ok(Err(\"fail\")))");
12161        assert_eq!(output, vec!["false"]);
12162        let output = run_output("print(is_err(Err(\"fail\")))");
12163        assert_eq!(output, vec!["true"]);
12164    }
12165
12166    #[test]
12167    fn test_vm_unwrap_ok() {
12168        let output = run_output("print(unwrap(Ok(42)))");
12169        assert_eq!(output, vec!["42"]);
12170    }
12171
12172    #[test]
12173    fn test_vm_unwrap_err_panics() {
12174        let result = run("unwrap(Err(\"fail\"))");
12175        assert!(result.is_err());
12176    }
12177
12178    #[test]
12179    fn test_vm_try_on_ok() {
12180        let output = run_output(
12181            r#"fn get_val() { Ok(42) }
12182fn process() { let v = get_val()? + 1
12183Ok(v) }
12184print(process())"#,
12185        );
12186        assert_eq!(output, vec!["Result::Ok(43)"]);
12187    }
12188
12189    #[test]
12190    fn test_vm_try_on_err_propagates() {
12191        let output = run_output(
12192            r#"fn failing() { Err("oops") }
12193fn process() { let v = failing()?
12194Ok(v) }
12195print(process())"#,
12196        );
12197        assert_eq!(output, vec!["Result::Err(oops)"]);
12198    }
12199
12200    #[test]
12201    fn test_vm_try_on_none_propagates() {
12202        let output = run_output(
12203            r#"fn get_none() { none }
12204fn process() { let v = get_none()?
1220542 }
12206print(process())"#,
12207        );
12208        assert_eq!(output, vec!["none"]);
12209    }
12210
12211    #[test]
12212    fn test_vm_try_passthrough() {
12213        // ? on a normal value should passthrough
12214        let output = run_output(
12215            r#"fn get_val() { 42 }
12216fn process() { let v = get_val()?
12217v + 1 }
12218print(process())"#,
12219        );
12220        assert_eq!(output, vec!["43"]);
12221    }
12222
12223    #[test]
12224    fn test_vm_result_match() {
12225        let output = run_output(
12226            r#"let r = Ok(42)
12227print(is_ok(r))
12228print(unwrap(r))"#,
12229        );
12230        assert_eq!(output, vec!["true", "42"]);
12231    }
12232
12233    #[test]
12234    fn test_vm_result_match_err() {
12235        let output = run_output(
12236            r#"let r = Err("fail")
12237print(is_err(r))
12238match r {
12239    Result::Err(e) => print("got error"),
12240    _ => print("no error")
12241}"#,
12242        );
12243        assert_eq!(output, vec!["true", "got error"]);
12244    }
12245
12246    // ── Set tests ──
12247
12248    #[test]
12249    fn test_vm_set_from_dedup() {
12250        let output = run_output(
12251            r#"let s = set_from([1, 2, 3, 2, 1])
12252print(len(s))
12253print(type_of(s))"#,
12254        );
12255        assert_eq!(output, vec!["3", "set"]);
12256    }
12257
12258    #[test]
12259    fn test_vm_set_add() {
12260        let output = run_output(
12261            r#"let s = set_from([1, 2])
12262let s2 = set_add(s, 3)
12263let s3 = set_add(s2, 2)
12264print(len(s2))
12265print(len(s3))"#,
12266        );
12267        assert_eq!(output, vec!["3", "3"]);
12268    }
12269
12270    #[test]
12271    fn test_vm_set_remove() {
12272        let output = run_output(
12273            r#"let s = set_from([1, 2, 3])
12274let s2 = set_remove(s, 2)
12275print(len(s2))
12276print(set_contains(s2, 2))"#,
12277        );
12278        assert_eq!(output, vec!["2", "false"]);
12279    }
12280
12281    #[test]
12282    fn test_vm_set_contains() {
12283        let output = run_output(
12284            r#"let s = set_from([1, 2, 3])
12285print(set_contains(s, 2))
12286print(set_contains(s, 5))"#,
12287        );
12288        assert_eq!(output, vec!["true", "false"]);
12289    }
12290
12291    #[test]
12292    fn test_vm_set_union() {
12293        let output = run_output(
12294            r#"let a = set_from([1, 2, 3])
12295let b = set_from([3, 4, 5])
12296let c = set_union(a, b)
12297print(len(c))"#,
12298        );
12299        assert_eq!(output, vec!["5"]);
12300    }
12301
12302    #[test]
12303    fn test_vm_set_intersection() {
12304        let output = run_output(
12305            r#"let a = set_from([1, 2, 3])
12306let b = set_from([2, 3, 4])
12307let c = set_intersection(a, b)
12308print(len(c))"#,
12309        );
12310        assert_eq!(output, vec!["2"]);
12311    }
12312
12313    #[test]
12314    fn test_vm_set_difference() {
12315        let output = run_output(
12316            r#"let a = set_from([1, 2, 3])
12317let b = set_from([2, 3, 4])
12318let c = set_difference(a, b)
12319print(len(c))"#,
12320        );
12321        assert_eq!(output, vec!["1"]);
12322    }
12323
12324    #[test]
12325    fn test_vm_set_for_loop() {
12326        let output = run_output(
12327            r#"let s = set_from([10, 20, 30])
12328let total = 0
12329for item in s {
12330    total = total + item
12331}
12332print(total)"#,
12333        );
12334        assert_eq!(output, vec!["60"]);
12335    }
12336
12337    #[test]
12338    fn test_vm_set_to_list() {
12339        let output = run_output(
12340            r#"let s = set_from([3, 1, 2])
12341let lst = s.to_list()
12342print(type_of(lst))
12343print(len(lst))"#,
12344        );
12345        assert_eq!(output, vec!["list", "3"]);
12346    }
12347
12348    #[test]
12349    fn test_vm_set_method_contains() {
12350        let output = run_output(
12351            r#"let s = set_from([1, 2, 3])
12352print(s.contains(2))
12353print(s.contains(5))"#,
12354        );
12355        assert_eq!(output, vec!["true", "false"]);
12356    }
12357
12358    #[test]
12359    fn test_vm_set_method_add_remove() {
12360        let output = run_output(
12361            r#"let s = set_from([1, 2])
12362let s2 = s.add(3)
12363print(s2.len())
12364let s3 = s2.remove(1)
12365print(s3.len())"#,
12366        );
12367        assert_eq!(output, vec!["3", "2"]);
12368    }
12369
12370    #[test]
12371    fn test_vm_set_method_union_intersection_difference() {
12372        let output = run_output(
12373            r#"let a = set_from([1, 2, 3])
12374let b = set_from([2, 3, 4])
12375print(a.union(b).len())
12376print(a.intersection(b).len())
12377print(a.difference(b).len())"#,
12378        );
12379        assert_eq!(output, vec!["4", "2", "1"]);
12380    }
12381
12382    #[test]
12383    fn test_vm_set_empty() {
12384        let output = run_output(
12385            r#"let s = set_from([])
12386print(len(s))
12387let s2 = s.add(1)
12388print(len(s2))"#,
12389        );
12390        assert_eq!(output, vec!["0", "1"]);
12391    }
12392
12393    #[test]
12394    fn test_vm_set_string_values() {
12395        let output = run_output(
12396            r#"let s = set_from(["a", "b", "a", "c"])
12397print(len(s))
12398print(s.contains("b"))"#,
12399        );
12400        assert_eq!(output, vec!["3", "true"]);
12401    }
12402
12403    // ── Phase 11: Module System VM Tests ──
12404
12405    #[test]
12406    fn test_vm_import_with_caching() {
12407        // Test that the VM has caching fields initialized
12408        let vm = Vm::new();
12409        assert!(vm.module_cache.is_empty());
12410        assert!(vm.importing_files.is_empty());
12411        assert!(vm.file_path.is_none());
12412    }
12413
12414    #[test]
12415    fn test_vm_use_single_file() {
12416        // Create a temp dir with module files
12417        let dir = tempfile::tempdir().unwrap();
12418        let lib_path = dir.path().join("math.tl");
12419        std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12420
12421        let main_path = dir.path().join("main.tl");
12422        std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12423
12424        let source = std::fs::read_to_string(&main_path).unwrap();
12425        let program = tl_parser::parse(&source).unwrap();
12426        let proto = crate::compiler::compile(&program).unwrap();
12427
12428        let mut vm = Vm::new();
12429        vm.file_path = Some(main_path.to_string_lossy().to_string());
12430        vm.execute(&proto).unwrap();
12431        assert_eq!(vm.output, vec!["3"]);
12432    }
12433
12434    #[test]
12435    fn test_vm_use_wildcard() {
12436        let dir = tempfile::tempdir().unwrap();
12437        std::fs::write(
12438            dir.path().join("helpers.tl"),
12439            "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12440        )
12441        .unwrap();
12442
12443        let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12444        let main_path = dir.path().join("main.tl");
12445        std::fs::write(&main_path, main_src).unwrap();
12446
12447        let program = tl_parser::parse(main_src).unwrap();
12448        let proto = crate::compiler::compile(&program).unwrap();
12449
12450        let mut vm = Vm::new();
12451        vm.file_path = Some(main_path.to_string_lossy().to_string());
12452        vm.execute(&proto).unwrap();
12453        assert_eq!(vm.output, vec!["hello", "bye"]);
12454    }
12455
12456    #[test]
12457    fn test_vm_use_aliased() {
12458        let dir = tempfile::tempdir().unwrap();
12459        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12460
12461        let main_src = "use mylib as m\nprint(m.compute())";
12462        let main_path = dir.path().join("main.tl");
12463        std::fs::write(&main_path, main_src).unwrap();
12464
12465        let program = tl_parser::parse(main_src).unwrap();
12466        let proto = crate::compiler::compile(&program).unwrap();
12467
12468        let mut vm = Vm::new();
12469        vm.file_path = Some(main_path.to_string_lossy().to_string());
12470        vm.execute(&proto).unwrap();
12471        assert_eq!(vm.output, vec!["42"]);
12472    }
12473
12474    #[test]
12475    fn test_vm_use_directory_module() {
12476        let dir = tempfile::tempdir().unwrap();
12477        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12478        std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12479
12480        let main_src = "use utils\nprint(helper())";
12481        let main_path = dir.path().join("main.tl");
12482        std::fs::write(&main_path, main_src).unwrap();
12483
12484        let program = tl_parser::parse(main_src).unwrap();
12485        let proto = crate::compiler::compile(&program).unwrap();
12486
12487        let mut vm = Vm::new();
12488        vm.file_path = Some(main_path.to_string_lossy().to_string());
12489        vm.execute(&proto).unwrap();
12490        assert_eq!(vm.output, vec!["99"]);
12491    }
12492
12493    #[test]
12494    fn test_vm_circular_import_detection() {
12495        let dir = tempfile::tempdir().unwrap();
12496        let a_path = dir.path().join("a.tl");
12497        let b_path = dir.path().join("b.tl");
12498        std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
12499        std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
12500
12501        let source = std::fs::read_to_string(&a_path).unwrap();
12502        let program = tl_parser::parse(&source).unwrap();
12503        let proto = crate::compiler::compile(&program).unwrap();
12504
12505        let mut vm = Vm::new();
12506        vm.file_path = Some(a_path.to_string_lossy().to_string());
12507        let result = vm.execute(&proto);
12508        assert!(result.is_err());
12509        assert!(format!("{:?}", result).contains("Circular import"));
12510    }
12511
12512    #[test]
12513    fn test_vm_module_caching() {
12514        // Import the same module twice — should use cache
12515        let dir = tempfile::tempdir().unwrap();
12516        std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
12517
12518        let main_src = "use cached\nuse cached\nprint(X)";
12519        let main_path = dir.path().join("main.tl");
12520        std::fs::write(&main_path, main_src).unwrap();
12521
12522        let program = tl_parser::parse(main_src).unwrap();
12523        let proto = crate::compiler::compile(&program).unwrap();
12524
12525        let mut vm = Vm::new();
12526        vm.file_path = Some(main_path.to_string_lossy().to_string());
12527        vm.execute(&proto).unwrap();
12528        assert_eq!(vm.output, vec!["42"]);
12529    }
12530
12531    #[test]
12532    fn test_vm_existing_import_still_works() {
12533        // Verify backward compat of classic import
12534        let dir = tempfile::tempdir().unwrap();
12535        let lib_path = dir.path().join("lib.tl");
12536        std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
12537
12538        let main_src = format!(
12539            "import \"{}\"\nprint(imported_fn())",
12540            lib_path.to_string_lossy()
12541        );
12542        let program = tl_parser::parse(&main_src).unwrap();
12543        let proto = crate::compiler::compile(&program).unwrap();
12544
12545        let mut vm = Vm::new();
12546        vm.execute(&proto).unwrap();
12547        assert_eq!(vm.output, vec!["123"]);
12548    }
12549
12550    #[test]
12551    fn test_vm_pub_fn_parsing() {
12552        // Pub fn should compile and run normally
12553        let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
12554        assert_eq!(output, vec!["3"]);
12555    }
12556
12557    #[test]
12558    fn test_vm_use_nested_path() {
12559        let dir = tempfile::tempdir().unwrap();
12560        std::fs::create_dir_all(dir.path().join("data")).unwrap();
12561        std::fs::write(
12562            dir.path().join("data/transforms.tl"),
12563            "fn clean(x) { x + 1 }",
12564        )
12565        .unwrap();
12566
12567        let main_src = "use data.transforms\nprint(clean(41))";
12568        let main_path = dir.path().join("main.tl");
12569        std::fs::write(&main_path, main_src).unwrap();
12570
12571        let program = tl_parser::parse(main_src).unwrap();
12572        let proto = crate::compiler::compile(&program).unwrap();
12573
12574        let mut vm = Vm::new();
12575        vm.file_path = Some(main_path.to_string_lossy().to_string());
12576        vm.execute(&proto).unwrap();
12577        assert_eq!(vm.output, vec!["42"]);
12578    }
12579
12580    // -- Integration tests: multi-file, backward compat, mixed --
12581
12582    #[test]
12583    fn test_integration_multi_file_use_functions() {
12584        // main.tl uses functions from lib.tl
12585        let dir = tempfile::tempdir().unwrap();
12586        std::fs::write(
12587            dir.path().join("lib.tl"),
12588            "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
12589        )
12590        .unwrap();
12591
12592        let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
12593        let main_path = dir.path().join("main.tl");
12594        std::fs::write(&main_path, main_src).unwrap();
12595
12596        let program = tl_parser::parse(main_src).unwrap();
12597        let proto = crate::compiler::compile(&program).unwrap();
12598        let mut vm = Vm::new();
12599        vm.file_path = Some(main_path.to_string_lossy().to_string());
12600        vm.execute(&proto).unwrap();
12601        assert_eq!(vm.output, vec!["Hello, World!", "42"]);
12602    }
12603
12604    #[test]
12605    fn test_integration_mixed_import_and_use() {
12606        // Combine classic import and use in same file
12607        let dir = tempfile::tempdir().unwrap();
12608        std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
12609        std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
12610
12611        let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
12612        let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
12613        let main_path = dir.path().join("main.tl");
12614        std::fs::write(&main_path, &main_src).unwrap();
12615
12616        let program = tl_parser::parse(&main_src).unwrap();
12617        let proto = crate::compiler::compile(&program).unwrap();
12618        let mut vm = Vm::new();
12619        vm.file_path = Some(main_path.to_string_lossy().to_string());
12620        vm.execute(&proto).unwrap();
12621        assert_eq!(vm.output, vec!["30"]);
12622    }
12623
12624    #[test]
12625    fn test_integration_directory_module_with_mod_tl() {
12626        // utils/mod.tl re-exports functions
12627        let dir = tempfile::tempdir().unwrap();
12628        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12629        std::fs::write(
12630            dir.path().join("utils/mod.tl"),
12631            "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
12632        )
12633        .unwrap();
12634
12635        let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
12636        let main_path = dir.path().join("main.tl");
12637        std::fs::write(&main_path, main_src).unwrap();
12638
12639        let program = tl_parser::parse(main_src).unwrap();
12640        let proto = crate::compiler::compile(&program).unwrap();
12641        let mut vm = Vm::new();
12642        vm.file_path = Some(main_path.to_string_lossy().to_string());
12643        vm.execute(&proto).unwrap();
12644        assert_eq!(vm.output, vec!["99", "42!"]);
12645    }
12646
12647    #[test]
12648    fn test_integration_circular_dep_error() {
12649        let dir = tempfile::tempdir().unwrap();
12650        let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
12651        let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
12652        std::fs::write(
12653            dir.path().join("a.tl"),
12654            format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
12655        )
12656        .unwrap();
12657        std::fs::write(
12658            dir.path().join("b.tl"),
12659            format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
12660        )
12661        .unwrap();
12662
12663        let main_src = format!("import \"{a_abs}\"");
12664        let program = tl_parser::parse(&main_src).unwrap();
12665        let proto = crate::compiler::compile(&program).unwrap();
12666        let mut vm = Vm::new();
12667        let result = vm.execute(&proto);
12668        assert!(result.is_err());
12669        let err_msg = format!("{}", result.unwrap_err());
12670        assert!(
12671            err_msg.contains("Circular") || err_msg.contains("circular"),
12672            "Expected circular import error, got: {err_msg}"
12673        );
12674    }
12675
12676    #[test]
12677    fn test_integration_use_aliased_method_call() {
12678        // use lib as m, then m.compute()
12679        let dir = tempfile::tempdir().unwrap();
12680        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12681
12682        let main_src = "use mylib as m\nprint(m.compute())";
12683        let main_path = dir.path().join("main.tl");
12684        std::fs::write(&main_path, main_src).unwrap();
12685
12686        let program = tl_parser::parse(main_src).unwrap();
12687        let proto = crate::compiler::compile(&program).unwrap();
12688        let mut vm = Vm::new();
12689        vm.file_path = Some(main_path.to_string_lossy().to_string());
12690        vm.execute(&proto).unwrap();
12691        assert_eq!(vm.output, vec!["42"]);
12692    }
12693
12694    #[test]
12695    fn test_integration_module_caching_shared() {
12696        // Import same module twice; second import uses cache, not re-execution
12697        let dir = tempfile::tempdir().unwrap();
12698        std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
12699
12700        let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
12701        let main_path = dir.path().join("main.tl");
12702        std::fs::write(&main_path, main_src).unwrap();
12703
12704        let program = tl_parser::parse(main_src).unwrap();
12705        let proto = crate::compiler::compile(&program).unwrap();
12706        let mut vm = Vm::new();
12707        vm.file_path = Some(main_path.to_string_lossy().to_string());
12708        vm.execute(&proto).unwrap();
12709        assert_eq!(vm.output, vec!["42", "42"]);
12710    }
12711
12712    #[test]
12713    fn test_integration_pub_keyword_in_module() {
12714        // pub fn in a module should work when imported
12715        let dir = tempfile::tempdir().unwrap();
12716        std::fs::write(
12717            dir.path().join("pubmod.tl"),
12718            "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
12719        )
12720        .unwrap();
12721
12722        let main_src = "use pubmod\nprint(public_fn())";
12723        let main_path = dir.path().join("main.tl");
12724        std::fs::write(&main_path, main_src).unwrap();
12725
12726        let program = tl_parser::parse(main_src).unwrap();
12727        let proto = crate::compiler::compile(&program).unwrap();
12728        let mut vm = Vm::new();
12729        vm.file_path = Some(main_path.to_string_lossy().to_string());
12730        vm.execute(&proto).unwrap();
12731        assert_eq!(vm.output, vec!["100"]);
12732    }
12733
12734    #[test]
12735    fn test_integration_backward_compat_import_as() {
12736        // Classic import-as syntax should still work
12737        let dir = tempfile::tempdir().unwrap();
12738        let lib_path = dir.path().join("mylib.tl");
12739        std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
12740
12741        let main_src = format!(
12742            "import \"{}\" as m\nprint(m.compute())",
12743            lib_path.to_string_lossy()
12744        );
12745        let program = tl_parser::parse(&main_src).unwrap();
12746        let proto = crate::compiler::compile(&program).unwrap();
12747        let mut vm = Vm::new();
12748        vm.execute(&proto).unwrap();
12749        assert_eq!(vm.output, vec!["77"]);
12750    }
12751
12752    // ── Phase 12: Generics & Traits (VM) ──────────────────
12753
12754    #[test]
12755    fn test_vm_generic_fn() {
12756        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
12757        assert_eq!(output, vec!["42"]);
12758    }
12759
12760    #[test]
12761    fn test_vm_generic_fn_string() {
12762        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
12763        assert_eq!(output, vec!["hello"]);
12764    }
12765
12766    #[test]
12767    fn test_vm_generic_struct() {
12768        let output = run_output(
12769            "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
12770        );
12771        assert_eq!(output, vec!["1", "hi"]);
12772    }
12773
12774    #[test]
12775    fn test_vm_trait_def_noop() {
12776        // Trait definitions should compile without error (no-op)
12777        let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
12778        assert_eq!(output, vec!["ok"]);
12779    }
12780
12781    #[test]
12782    fn test_vm_trait_impl_methods() {
12783        let output = run_output(
12784            "struct Point { x: int, y: int }\nimpl Display for Point { fn show(self) -> string { \"point\" } }\nlet p = Point { x: 1, y: 2 }\nprint(p.show())",
12785        );
12786        assert_eq!(output, vec!["point"]);
12787    }
12788
12789    #[test]
12790    fn test_vm_generic_enum() {
12791        // Generic enum declaration works — type params are erased at runtime
12792        let output = run_output(
12793            "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
12794        );
12795        assert_eq!(output, vec!["enum"]);
12796    }
12797
12798    #[test]
12799    fn test_vm_where_clause_runtime() {
12800        // Where clause is compile-time only; function still works at runtime
12801        let output =
12802            run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
12803        assert_eq!(output, vec!["10"]);
12804    }
12805
12806    #[test]
12807    fn test_vm_trait_impl_self_method() {
12808        let output = run_output(
12809            "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
12810        );
12811        assert_eq!(output, vec!["6"]);
12812    }
12813
12814    // ── Phase 12: Integration tests ──────────────────────────
12815
12816    #[test]
12817    fn test_vm_generic_fn_with_type_inference() {
12818        // Generic function called with different types
12819        let output = run_output(
12820            "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
12821        );
12822        assert_eq!(output, vec!["1", "a"]);
12823    }
12824
12825    #[test]
12826    fn test_vm_generic_struct_with_methods() {
12827        let output = run_output(
12828            "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
12829        );
12830        assert_eq!(output, vec!["42"]);
12831    }
12832
12833    #[test]
12834    fn test_vm_trait_def_impl_call() {
12835        let output = run_output(
12836            "trait Greetable { fn greet(self) -> string }\nstruct Person { name: string }\nimpl Greetable for Person { fn greet(self) -> string { self.name } }\nlet p = Person { name: \"Alice\" }\nprint(p.greet())",
12837        );
12838        assert_eq!(output, vec!["Alice"]);
12839    }
12840
12841    #[test]
12842    fn test_vm_multiple_generic_params() {
12843        let output = run_output(
12844            "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
12845        );
12846        assert_eq!(output, vec!["2"]);
12847    }
12848
12849    #[test]
12850    fn test_vm_backward_compat_non_generic() {
12851        // Existing non-generic code must still work unchanged
12852        let output = run_output(
12853            "fn add(a, b) { a + b }\nstruct Point { x: int, y: int }\nimpl Point { fn sum(self) { self.x + self.y } }\nlet p = Point { x: 3, y: 4 }\nprint(add(1, 2))\nprint(p.sum())",
12854        );
12855        assert_eq!(output, vec!["3", "7"]);
12856    }
12857
12858    // ── Phase 16: Package import resolution tests ──
12859
12860    #[test]
12861    fn test_vm_package_import_resolves() {
12862        // Create a test package on disk
12863        let tmp = tempfile::tempdir().unwrap();
12864        let pkg_dir = tmp.path().join("mylib");
12865        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12866        std::fs::write(
12867            pkg_dir.join("src/lib.tl"),
12868            "pub fn greet() { print(\"hello from pkg\") }",
12869        )
12870        .unwrap();
12871        std::fs::write(
12872            pkg_dir.join("tl.toml"),
12873            "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
12874        )
12875        .unwrap();
12876
12877        // use X imports all exports wildcard-style; call greet() directly
12878        let main_file = tmp.path().join("main.tl");
12879        std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
12880
12881        let source = std::fs::read_to_string(&main_file).unwrap();
12882        let program = tl_parser::parse(&source).unwrap();
12883        let proto = crate::compiler::compile(&program).unwrap();
12884
12885        let mut vm = Vm::new();
12886        vm.file_path = Some(main_file.to_string_lossy().to_string());
12887        vm.package_roots.insert("mylib".into(), pkg_dir);
12888        vm.execute(&proto).unwrap();
12889
12890        assert_eq!(vm.output, vec!["hello from pkg"]);
12891    }
12892
12893    #[test]
12894    fn test_vm_package_nested_import() {
12895        let tmp = tempfile::tempdir().unwrap();
12896        let pkg_dir = tmp.path().join("utils");
12897        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12898        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12899        std::fs::write(
12900            pkg_dir.join("tl.toml"),
12901            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12902        )
12903        .unwrap();
12904
12905        // use utils.math wildcard-imports math.tl contents
12906        let main_file = tmp.path().join("main.tl");
12907        std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
12908
12909        let source = std::fs::read_to_string(&main_file).unwrap();
12910        let program = tl_parser::parse(&source).unwrap();
12911        let proto = crate::compiler::compile(&program).unwrap();
12912
12913        let mut vm = Vm::new();
12914        vm.file_path = Some(main_file.to_string_lossy().to_string());
12915        vm.package_roots.insert("utils".into(), pkg_dir);
12916        vm.execute(&proto).unwrap();
12917
12918        assert_eq!(vm.output, vec!["42"]);
12919    }
12920
12921    #[test]
12922    fn test_vm_package_aliased_import() {
12923        let tmp = tempfile::tempdir().unwrap();
12924        let pkg_dir = tmp.path().join("utils");
12925        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12926        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12927        std::fs::write(
12928            pkg_dir.join("tl.toml"),
12929            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12930        )
12931        .unwrap();
12932
12933        // use X as Y creates a namespaced module object
12934        let main_file = tmp.path().join("main.tl");
12935        std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
12936
12937        let source = std::fs::read_to_string(&main_file).unwrap();
12938        let program = tl_parser::parse(&source).unwrap();
12939        let proto = crate::compiler::compile(&program).unwrap();
12940
12941        let mut vm = Vm::new();
12942        vm.file_path = Some(main_file.to_string_lossy().to_string());
12943        vm.package_roots.insert("utils".into(), pkg_dir);
12944        vm.execute(&proto).unwrap();
12945
12946        assert_eq!(vm.output, vec!["42"]);
12947    }
12948
12949    #[test]
12950    fn test_vm_package_underscore_to_hyphen() {
12951        let tmp = tempfile::tempdir().unwrap();
12952        let pkg_dir = tmp.path().join("my-pkg");
12953        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12954        std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
12955        std::fs::write(
12956            pkg_dir.join("tl.toml"),
12957            "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
12958        )
12959        .unwrap();
12960
12961        // TL identifiers use underscores, package names use hyphens
12962        let main_file = tmp.path().join("main.tl");
12963        std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
12964
12965        let source = std::fs::read_to_string(&main_file).unwrap();
12966        let program = tl_parser::parse(&source).unwrap();
12967        let proto = crate::compiler::compile(&program).unwrap();
12968
12969        let mut vm = Vm::new();
12970        vm.file_path = Some(main_file.to_string_lossy().to_string());
12971        vm.package_roots.insert("my-pkg".into(), pkg_dir);
12972        vm.execute(&proto).unwrap();
12973
12974        assert_eq!(vm.output, vec!["99"]);
12975    }
12976
12977    #[test]
12978    fn test_vm_local_module_priority_over_package() {
12979        // Local modules should take priority over packages
12980        let tmp = tempfile::tempdir().unwrap();
12981
12982        // Create a local module
12983        std::fs::write(
12984            tmp.path().join("mymod.tl"),
12985            "pub fn val() { print(\"local\") }",
12986        )
12987        .unwrap();
12988
12989        // Create a package with the same name
12990        let pkg_dir = tmp.path().join("pkg_mymod");
12991        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12992        std::fs::write(
12993            pkg_dir.join("src/lib.tl"),
12994            "pub fn val() { print(\"package\") }",
12995        )
12996        .unwrap();
12997
12998        // use mymod → wildcard imports, val() available directly
12999        let main_file = tmp.path().join("main.tl");
13000        std::fs::write(&main_file, "use mymod\nval()").unwrap();
13001
13002        let source = std::fs::read_to_string(&main_file).unwrap();
13003        let program = tl_parser::parse(&source).unwrap();
13004        let proto = crate::compiler::compile(&program).unwrap();
13005
13006        let mut vm = Vm::new();
13007        vm.file_path = Some(main_file.to_string_lossy().to_string());
13008        vm.package_roots.insert("mymod".into(), pkg_dir);
13009        vm.execute(&proto).unwrap();
13010
13011        // Local module should win
13012        assert_eq!(vm.output, vec!["local"]);
13013    }
13014
13015    #[test]
13016    fn test_vm_package_missing_error() {
13017        let tmp = tempfile::tempdir().unwrap();
13018        let main_file = tmp.path().join("main.tl");
13019        std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
13020
13021        let source = std::fs::read_to_string(&main_file).unwrap();
13022        let program = tl_parser::parse(&source).unwrap();
13023        let proto = crate::compiler::compile(&program).unwrap();
13024
13025        let mut vm = Vm::new();
13026        vm.file_path = Some(main_file.to_string_lossy().to_string());
13027        let result = vm.execute(&proto);
13028
13029        assert!(result.is_err());
13030        let err = format!("{:?}", result.unwrap_err());
13031        assert!(err.contains("Module not found"));
13032    }
13033
13034    #[test]
13035    #[cfg(feature = "native")]
13036    fn test_resolve_package_file_entry_points() {
13037        let tmp = tempfile::tempdir().unwrap();
13038
13039        // Test src/lib.tl entry point
13040        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
13041        std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
13042        let result = resolve_package_file(tmp.path(), &[]);
13043        assert!(result.is_some());
13044        assert!(result.unwrap().contains("lib.tl"));
13045
13046        // Test nested module in src/
13047        std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
13048        let result = resolve_package_file(tmp.path(), &["math"]);
13049        assert!(result.is_some());
13050        assert!(result.unwrap().contains("math.tl"));
13051    }
13052
13053    #[test]
13054    fn test_vm_package_propagates_to_sub_imports() {
13055        // Package roots should be available in sub-VM during imports
13056        let tmp = tempfile::tempdir().unwrap();
13057
13058        // Create a package
13059        let pkg_dir = tmp.path().join("helpers");
13060        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13061        std::fs::write(
13062            pkg_dir.join("src/lib.tl"),
13063            "pub fn help() { print(\"helped\") }",
13064        )
13065        .unwrap();
13066        std::fs::write(
13067            pkg_dir.join("tl.toml"),
13068            "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13069        )
13070        .unwrap();
13071
13072        // Create a local module that imports from the package (wildcard then calls directly)
13073        std::fs::write(
13074            tmp.path().join("bridge.tl"),
13075            "use helpers\npub fn run() { help() }",
13076        )
13077        .unwrap();
13078
13079        // use bridge wildcard-imports run(), then call it
13080        let main_file = tmp.path().join("main.tl");
13081        std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13082
13083        let source = std::fs::read_to_string(&main_file).unwrap();
13084        let program = tl_parser::parse(&source).unwrap();
13085        let proto = crate::compiler::compile(&program).unwrap();
13086
13087        let mut vm = Vm::new();
13088        vm.file_path = Some(main_file.to_string_lossy().to_string());
13089        vm.package_roots.insert("helpers".into(), pkg_dir);
13090        vm.execute(&proto).unwrap();
13091
13092        assert_eq!(vm.output, vec!["helped"]);
13093    }
13094
13095    // ── Phase 18: Closures & Lambdas Improvements ────────────────
13096
13097    #[test]
13098    fn test_block_body_closure_basic() {
13099        let output =
13100            run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13101        assert_eq!(output, vec!["11"]);
13102    }
13103
13104    #[test]
13105    fn test_block_body_closure_captures_upvalue() {
13106        let output = run_output(
13107            "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13108        );
13109        assert_eq!(output, vec!["15"]);
13110    }
13111
13112    #[test]
13113    fn test_block_body_closure_as_hof_arg() {
13114        let output = run_output(
13115            "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13116        );
13117        assert_eq!(output, vec!["[3, 5, 7]"]);
13118    }
13119
13120    #[test]
13121    fn test_block_body_closure_multi_stmt() {
13122        let output = run_output(
13123            "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13124        );
13125        assert_eq!(output, vec!["19"]);
13126    }
13127
13128    #[test]
13129    fn test_type_alias_noop() {
13130        // Type alias should be a no-op at runtime, code using aliased types should work
13131        let output = run_output(
13132            "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13133        );
13134        assert_eq!(output, vec!["10"]);
13135    }
13136
13137    #[test]
13138    fn test_type_alias_in_function_sig() {
13139        let output = run_output(
13140            "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13141        );
13142        assert_eq!(output, vec!["15"]);
13143    }
13144
13145    #[test]
13146    fn test_shorthand_closure() {
13147        let output = run_output("let double = x => x * 2\nprint(double(5))");
13148        assert_eq!(output, vec!["10"]);
13149    }
13150
13151    #[test]
13152    fn test_shorthand_closure_in_map() {
13153        let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13154        assert_eq!(output, vec!["[2, 4, 6]"]);
13155    }
13156
13157    #[test]
13158    fn test_iife() {
13159        let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13160        assert_eq!(output, vec!["10"]);
13161    }
13162
13163    #[test]
13164    fn test_hof_apply() {
13165        let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13166        assert_eq!(output, vec!["15"]);
13167    }
13168
13169    #[test]
13170    fn test_closure_stored_in_list() {
13171        let output = run_output(
13172            "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13173        );
13174        assert_eq!(output, vec!["6", "10"]);
13175    }
13176
13177    #[test]
13178    fn test_block_body_closure_with_return() {
13179        // Use explicit return statements since if/else is a statement, not a tail expression
13180        let output = run_output(
13181            "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13182        );
13183        assert_eq!(output, vec!["positive", "non-positive"]);
13184    }
13185
13186    #[test]
13187    fn test_shorthand_closure_in_filter() {
13188        let output = run_output(
13189            "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13190        );
13191        assert_eq!(output, vec!["[2, 4, 6]"]);
13192    }
13193
13194    #[test]
13195    fn test_block_closure_with_multiple_returns() {
13196        let output = run_output(
13197            "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13198        );
13199        assert_eq!(output, vec!["5", "3"]);
13200    }
13201
13202    #[test]
13203    fn test_type_alias_with_block_closure() {
13204        let output = run_output(
13205            "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13206        );
13207        assert_eq!(output, vec!["10"]);
13208    }
13209
13210    #[test]
13211    fn test_closure_both_backends_expr() {
13212        // Same test, just verify VM works correctly
13213        let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13214        assert_eq!(output, vec!["13"]);
13215    }
13216
13217    // Phase 20: Python FFI feature-disabled test
13218    #[test]
13219    #[cfg(not(feature = "python"))]
13220    fn test_py_feature_disabled() {
13221        let result = run("py_import(\"math\")");
13222        assert!(result.is_err());
13223        let msg = format!("{}", result.unwrap_err());
13224        assert!(msg.contains("python") && msg.contains("feature"));
13225    }
13226
13227    #[test]
13228    #[cfg(feature = "python")]
13229    fn test_vm_py_import_and_eval() {
13230        pyo3::prepare_freethreaded_python();
13231        let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13232        assert_eq!(output.len(), 1);
13233        let pi: f64 = output[0].parse().unwrap();
13234        assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13235    }
13236
13237    #[test]
13238    #[cfg(feature = "python")]
13239    fn test_vm_py_eval_arithmetic() {
13240        pyo3::prepare_freethreaded_python();
13241        let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13242        assert_eq!(output, vec!["1024"]);
13243    }
13244
13245    #[test]
13246    #[cfg(feature = "python")]
13247    fn test_vm_py_method_dispatch() {
13248        pyo3::prepare_freethreaded_python();
13249        let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13250        assert_eq!(output, vec!["5.0"]);
13251    }
13252
13253    #[test]
13254    #[cfg(feature = "python")]
13255    fn test_vm_py_list_conversion() {
13256        pyo3::prepare_freethreaded_python();
13257        let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13258        assert_eq!(output, vec!["[10, 20, 30]"]);
13259    }
13260
13261    #[test]
13262    #[cfg(feature = "python")]
13263    fn test_vm_py_none_conversion() {
13264        pyo3::prepare_freethreaded_python();
13265        let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13266        assert_eq!(output, vec!["none"]);
13267    }
13268
13269    #[test]
13270    #[cfg(feature = "python")]
13271    fn test_vm_py_error_msg_quality() {
13272        pyo3::prepare_freethreaded_python();
13273        let result = run("py_import(\"nonexistent_xyz_module\")");
13274        assert!(result.is_err());
13275        let msg = format!("{}", result.unwrap_err());
13276        assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13277    }
13278
13279    #[test]
13280    #[cfg(feature = "python")]
13281    fn test_vm_py_getattr_setattr() {
13282        pyo3::prepare_freethreaded_python();
13283        let output = run_output(
13284            "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13285        );
13286        assert_eq!(output, vec!["99"]);
13287    }
13288
13289    #[test]
13290    #[cfg(feature = "python")]
13291    fn test_vm_py_callable_round_trip() {
13292        pyo3::prepare_freethreaded_python();
13293        let output = run_output(
13294            "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13295        );
13296        assert_eq!(output, vec!["3"]);
13297    }
13298
13299    // ── Phase 21: Schema Evolution VM tests ──
13300
13301    #[test]
13302    fn test_vm_schema_register_and_get() {
13303        let source = r#"let fields = map_from("id", "int64", "name", "string")
13304schema_register("User", 1, fields)
13305let result = schema_get("User", 1)
13306print(len(result))"#;
13307        let output = run_output(source);
13308        assert_eq!(output, vec!["2"]);
13309    }
13310
13311    #[test]
13312    fn test_vm_schema_latest() {
13313        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13314schema_register("User", 2, map_from("id", "int64", "name", "string"))
13315let latest = schema_latest("User")
13316print(latest)"#;
13317        let output = run_output(source);
13318        assert_eq!(output, vec!["2"]);
13319    }
13320
13321    #[test]
13322    fn test_vm_schema_history() {
13323        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13324schema_register("User", 2, map_from("id", "int64", "name", "string"))
13325let hist = schema_history("User")
13326print(len(hist))"#;
13327        let output = run_output(source);
13328        assert_eq!(output, vec!["2"]);
13329    }
13330
13331    #[test]
13332    fn test_vm_schema_check_backward_compat() {
13333        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13334schema_register("User", 2, map_from("id", "int64", "name", "string"))
13335let issues = schema_check("User", 1, 2, "backward")
13336print(len(issues))"#;
13337        let output = run_output(source);
13338        assert_eq!(output, vec!["0"]);
13339    }
13340
13341    #[test]
13342    fn test_vm_schema_diff() {
13343        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13344schema_register("User", 2, map_from("id", "int64", "name", "string"))
13345let diffs = schema_diff("User", 1, 2)
13346print(len(diffs))"#;
13347        let output = run_output(source);
13348        assert_eq!(output, vec!["1"]);
13349    }
13350
13351    #[test]
13352    fn test_vm_schema_versions() {
13353        let source = r#"schema_register("T", 1, map_from("id", "int64"))
13354schema_register("T", 3, map_from("id", "int64"))
13355schema_register("T", 2, map_from("id", "int64"))
13356let vers = schema_versions("T")
13357print(len(vers))"#;
13358        let output = run_output(source);
13359        assert_eq!(output, vec!["3"]);
13360    }
13361
13362    #[test]
13363    fn test_vm_schema_fields() {
13364        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13365let fields = schema_fields("User", 1)
13366print(len(fields))"#;
13367        let output = run_output(source);
13368        assert_eq!(output, vec!["2"]);
13369    }
13370
13371    #[test]
13372    fn test_vm_compile_versioned_schema() {
13373        let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13374        let output = run_output(source);
13375        assert!(output[0].contains("__schema__:User:v1:"));
13376    }
13377
13378    #[test]
13379    fn test_vm_compile_migrate() {
13380        let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13381        let output = run_output(source);
13382        assert_eq!(output, vec!["ok"]);
13383    }
13384
13385    #[test]
13386    fn test_vm_schema_check_backward_compat_fails() {
13387        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13388schema_register("User", 2, map_from("id", "int64"))
13389let issues = schema_check("User", 1, 2, "backward")
13390print(len(issues))"#;
13391        let output = run_output(source);
13392        assert_eq!(output, vec!["1"]);
13393    }
13394
13395    // ── Phase 22: Decimal VM Tests ─────────────────────────────────
13396
13397    #[test]
13398    fn test_vm_decimal_literal_and_arithmetic() {
13399        let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13400        assert_eq!(output, vec!["13.0", "26.25"]);
13401    }
13402
13403    #[test]
13404    fn test_vm_decimal_div_by_zero() {
13405        let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13406        let program = tl_parser::parse(source).unwrap();
13407        let proto = crate::compile(&program).unwrap();
13408        let mut vm = Vm::new();
13409        let result = vm.execute(&proto);
13410        assert!(result.is_err());
13411    }
13412
13413    #[test]
13414    fn test_vm_decimal_comparison_ops() {
13415        let output =
13416            run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13417        assert_eq!(output, vec!["true", "false", "true"]);
13418    }
13419
13420    // ── Phase 23: Security VM Tests ────────────────────────────────
13421
13422    #[test]
13423    fn test_vm_secret_vault_crud() {
13424        let output = run_output(
13425            "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13426        );
13427        assert_eq!(output, vec!["***", "none"]);
13428    }
13429
13430    #[test]
13431    fn test_vm_mask_email_basic() {
13432        let output = run_output("print(mask_email(\"alice@domain.com\"))");
13433        assert_eq!(output, vec!["a***@domain.com"]);
13434    }
13435
13436    #[test]
13437    fn test_vm_mask_phone_basic() {
13438        let output = run_output("print(mask_phone(\"123-456-7890\"))");
13439        assert_eq!(output, vec!["***-***-7890"]);
13440    }
13441
13442    #[test]
13443    fn test_vm_mask_cc_basic() {
13444        let output = run_output("print(mask_cc(\"4111222233334444\"))");
13445        assert_eq!(output, vec!["****-****-****-4444"]);
13446    }
13447
13448    #[test]
13449    fn test_vm_hash_produces_hex() {
13450        let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13451        assert_eq!(output, vec!["64"]);
13452    }
13453
13454    #[test]
13455    fn test_vm_redact_modes() {
13456        let output =
13457            run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13458        assert_eq!(output, vec!["***", "h***o"]);
13459    }
13460
13461    #[test]
13462    fn test_vm_security_policy_sandbox() {
13463        let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13464        let program = tl_parser::parse(source).unwrap();
13465        let proto = crate::compile(&program).unwrap();
13466        let mut vm = Vm::new();
13467        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13468        vm.execute(&proto).unwrap();
13469        assert_eq!(vm.output, vec!["false", "true"]);
13470    }
13471
13472    // ── Phase 25: Async Runtime Tests (feature-gated) ──────────────
13473
13474    #[cfg(feature = "async-runtime")]
13475    #[test]
13476    fn test_vm_async_read_write_file() {
13477        let dir = tempfile::tempdir().unwrap();
13478        let path = dir.path().join("async_test.txt");
13479        let path_str = path.to_str().unwrap().replace('\\', "/");
13480        let source = format!(
13481            r#"let wt = async_write_file("{path_str}", "async hello")
13482let wr = await(wt)
13483let rt = async_read_file("{path_str}")
13484let content = await(rt)
13485print(content)"#
13486        );
13487        let output = run_output(&source);
13488        assert_eq!(output, vec!["async hello"]);
13489    }
13490
13491    #[cfg(feature = "async-runtime")]
13492    #[test]
13493    fn test_vm_async_sleep() {
13494        let source = r#"
13495let t = async_sleep(10)
13496let r = await(t)
13497print(r)
13498"#;
13499        let output = run_output(source);
13500        assert_eq!(output, vec!["none"]);
13501    }
13502
13503    #[cfg(feature = "async-runtime")]
13504    #[test]
13505    fn test_vm_select_first_wins() {
13506        // select between a fast sleep and a slow sleep — fast one wins
13507        let source = r#"
13508let fast = async_sleep(10)
13509let slow = async_sleep(5000)
13510let winner = select(fast, slow)
13511let result = await(winner)
13512print(result)
13513"#;
13514        let output = run_output(source);
13515        assert_eq!(output, vec!["none"]);
13516    }
13517
13518    #[cfg(feature = "async-runtime")]
13519    #[test]
13520    fn test_vm_race_all() {
13521        let source = r#"
13522let t1 = async_sleep(10)
13523let t2 = async_sleep(5000)
13524let winner = race_all([t1, t2])
13525let result = await(winner)
13526print(result)
13527"#;
13528        let output = run_output(source);
13529        assert_eq!(output, vec!["none"]);
13530    }
13531
13532    #[cfg(feature = "async-runtime")]
13533    #[test]
13534    fn test_vm_async_map() {
13535        let source = r#"
13536let items = [1, 2, 3]
13537let t = async_map(items, (x) => x * 10)
13538let result = await(t)
13539print(result)
13540"#;
13541        let output = run_output(source);
13542        assert_eq!(output, vec!["[10, 20, 30]"]);
13543    }
13544
13545    #[cfg(feature = "async-runtime")]
13546    #[test]
13547    fn test_vm_async_filter() {
13548        let source = r#"
13549let items = [1, 2, 3, 4, 5]
13550let t = async_filter(items, (x) => x > 3)
13551let result = await(t)
13552print(result)
13553"#;
13554        let output = run_output(source);
13555        assert_eq!(output, vec!["[4, 5]"]);
13556    }
13557
13558    #[cfg(feature = "async-runtime")]
13559    #[test]
13560    fn test_vm_async_write_file_returns_none() {
13561        let dir = tempfile::tempdir().unwrap();
13562        let path = dir.path().join("write_test.txt");
13563        let path_str = path.to_str().unwrap().replace('\\', "/");
13564        let source = format!(
13565            r#"let t = async_write_file("{path_str}", "test data")
13566let r = await(t)
13567print(r)"#
13568        );
13569        let output = run_output(&source);
13570        assert_eq!(output, vec!["none"]);
13571    }
13572
13573    #[cfg(feature = "async-runtime")]
13574    #[test]
13575    fn test_vm_async_security_policy_blocks_write() {
13576        let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
13577        let program = tl_parser::parse(source).unwrap();
13578        let proto = crate::compile(&program).unwrap();
13579        let mut vm = Vm::new();
13580        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13581        let result = vm.execute(&proto);
13582        assert!(result.is_err());
13583        let err = format!("{}", result.unwrap_err());
13584        assert!(
13585            err.contains("file_write not allowed"),
13586            "Expected security error, got: {err}"
13587        );
13588    }
13589
13590    #[cfg(feature = "async-runtime")]
13591    #[test]
13592    fn test_vm_async_security_policy_allows_read() {
13593        // Sandbox allows file_read, so async_read_file should succeed (even if file doesn't exist)
13594        let dir = tempfile::tempdir().unwrap();
13595        let path = dir.path().join("readable.txt");
13596        std::fs::write(&path, "safe content").unwrap();
13597        let path_str = path.to_str().unwrap().replace('\\', "/");
13598        let source = format!(
13599            r#"let t = async_read_file("{path_str}")
13600let r = await(t)
13601print(r)"#
13602        );
13603        let program = tl_parser::parse(&source).unwrap();
13604        let proto = crate::compile(&program).unwrap();
13605        let mut vm = Vm::new();
13606        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13607        vm.execute(&proto).unwrap();
13608        assert_eq!(vm.output, vec!["safe content"]);
13609    }
13610
13611    #[cfg(feature = "async-runtime")]
13612    #[test]
13613    fn test_vm_async_map_empty_list() {
13614        let source = r#"
13615let t = async_map([], (x) => x * 2)
13616let result = await(t)
13617print(result)
13618"#;
13619        let output = run_output(source);
13620        assert_eq!(output, vec!["[]"]);
13621    }
13622
13623    #[cfg(feature = "async-runtime")]
13624    #[test]
13625    fn test_vm_async_filter_none_match() {
13626        let source = r#"
13627let t = async_filter([1, 2, 3], (x) => x > 100)
13628let result = await(t)
13629print(result)
13630"#;
13631        let output = run_output(source);
13632        assert_eq!(output, vec!["[]"]);
13633    }
13634
13635    // --- Phase 26: Closure upvalue closing tests ---
13636
13637    #[test]
13638    fn test_vm_closure_returned_from_function() {
13639        let output = run_output(
13640            r#"
13641fn make_adder(n) {
13642    return (x) => x + n
13643}
13644let add5 = make_adder(5)
13645print(add5(3))
13646print(add5(10))
13647"#,
13648        );
13649        assert_eq!(output, vec!["8", "15"]);
13650    }
13651
13652    #[test]
13653    fn test_vm_closure_factory_multiple_calls() {
13654        let output = run_output(
13655            r#"
13656fn make_adder(n) {
13657    return (x) => x + n
13658}
13659let add2 = make_adder(2)
13660let add10 = make_adder(10)
13661print(add2(5))
13662print(add10(5))
13663print(add2(1))
13664"#,
13665        );
13666        assert_eq!(output, vec!["7", "15", "3"]);
13667    }
13668
13669    #[test]
13670    fn test_vm_closure_returned_in_list() {
13671        let output = run_output(
13672            r#"
13673fn make_ops(n) {
13674    let add = (x) => x + n
13675    let mul = (x) => x * n
13676    return [add, mul]
13677}
13678let ops = make_ops(3)
13679print(ops[0](10))
13680print(ops[1](10))
13681"#,
13682        );
13683        assert_eq!(output, vec!["13", "30"]);
13684    }
13685
13686    #[test]
13687    fn test_vm_nested_closure_return() {
13688        let output = run_output(
13689            r#"
13690fn outer(a) {
13691    fn inner(b) {
13692        return (x) => x + a + b
13693    }
13694    return inner(10)
13695}
13696let f = outer(5)
13697print(f(1))
13698"#,
13699        );
13700        assert_eq!(output, vec!["16"]);
13701    }
13702
13703    #[test]
13704    fn test_vm_multiple_closures_same_local() {
13705        let output = run_output(
13706            r#"
13707fn make_pair(n) {
13708    let inc = (x) => x + n
13709    let dec = (x) => x - n
13710    return [inc, dec]
13711}
13712let pair = make_pair(7)
13713print(pair[0](10))
13714print(pair[1](10))
13715"#,
13716        );
13717        assert_eq!(output, vec!["17", "3"]);
13718    }
13719
13720    #[test]
13721    fn test_vm_closure_captures_multiple_locals() {
13722        let output = run_output(
13723            r#"
13724fn make_greeter(greeting, name) {
13725    let sep = " "
13726    return () => greeting + sep + name
13727}
13728let hi = make_greeter("Hello", "World")
13729let bye = make_greeter("Goodbye", "Alice")
13730print(hi())
13731print(bye())
13732"#,
13733        );
13734        assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
13735    }
13736
13737    // ── Phase 27: Data Error Hierarchy tests ──
13738
13739    #[test]
13740    fn test_vm_throw_catch_preserves_enum() {
13741        let output = run_output(
13742            r#"
13743enum Color { Red, Green(x) }
13744try {
13745    throw Color::Green(42)
13746} catch e {
13747    match e {
13748        Color::Green(x) => print(x),
13749        _ => print("no match"),
13750    }
13751}
13752"#,
13753        );
13754        assert_eq!(output, vec!["42"]);
13755    }
13756
13757    #[test]
13758    fn test_vm_throw_catch_string_compat() {
13759        let output = run_output(
13760            r#"
13761try {
13762    throw "hello error"
13763} catch e {
13764    print(e)
13765}
13766"#,
13767        );
13768        assert_eq!(output, vec!["hello error"]);
13769    }
13770
13771    #[test]
13772    fn test_vm_runtime_error_still_string() {
13773        let output = run_output(
13774            r#"
13775try {
13776    let x = 1 / 0
13777} catch e {
13778    print(type_of(e))
13779}
13780"#,
13781        );
13782        assert_eq!(output, vec!["string"]);
13783    }
13784
13785    #[test]
13786    fn test_vm_data_error_construct_and_throw() {
13787        let output = run_output(
13788            r#"
13789try {
13790    throw DataError::ParseError("bad format", "file.csv")
13791} catch e {
13792    print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
13793    print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
13794}
13795"#,
13796        );
13797        assert_eq!(output, vec!["bad format", "file.csv"]);
13798    }
13799
13800    #[test]
13801    fn test_vm_network_error_construct() {
13802        let output = run_output(
13803            r#"
13804let err = NetworkError::TimeoutError("timed out")
13805match err {
13806    NetworkError::TimeoutError(msg) => print(msg),
13807    _ => print("no match"),
13808}
13809"#,
13810        );
13811        assert_eq!(output, vec!["timed out"]);
13812    }
13813
13814    #[test]
13815    fn test_vm_connector_error_construct() {
13816        let output = run_output(
13817            r#"
13818let err = ConnectorError::AuthError("invalid creds", "postgres")
13819print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
13820print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
13821"#,
13822        );
13823        assert_eq!(output, vec!["invalid creds", "postgres"]);
13824    }
13825
13826    #[test]
13827    fn test_vm_is_error_builtin() {
13828        let output = run_output(
13829            r#"
13830let e1 = DataError::NotFound("users")
13831let e2 = NetworkError::TimeoutError("slow")
13832let e3 = ConnectorError::ConfigError("bad", "redis")
13833let e4 = "not an error"
13834print(is_error(e1))
13835print(is_error(e2))
13836print(is_error(e3))
13837print(is_error(e4))
13838"#,
13839        );
13840        assert_eq!(output, vec!["true", "true", "true", "false"]);
13841    }
13842
13843    #[test]
13844    fn test_vm_error_type_builtin() {
13845        let output = run_output(
13846            r#"
13847let e1 = DataError::ParseError("bad", "x.csv")
13848let e2 = NetworkError::HttpError("fail", "url")
13849let e3 = "not an error"
13850print(error_type(e1))
13851print(error_type(e2))
13852print(error_type(e3))
13853"#,
13854        );
13855        assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
13856    }
13857
13858    #[test]
13859    fn test_vm_match_error_variants() {
13860        let output = run_output(
13861            r#"
13862fn handle(err) {
13863    print(match err {
13864        DataError::ParseError(msg, _) => "parse: " + msg,
13865        DataError::SchemaError(msg, _, _) => "schema: " + msg,
13866        DataError::ValidationError(_, field) => "validation: " + field,
13867        DataError::NotFound(name) => "not found: " + name,
13868        _ => "unknown"
13869    })
13870}
13871handle(DataError::ParseError("bad csv", "data.csv"))
13872handle(DataError::NotFound("users_table"))
13873handle(DataError::SchemaError("mismatch", "int", "string"))
13874handle(DataError::ValidationError("invalid", "email"))
13875"#,
13876        );
13877        assert_eq!(
13878            output,
13879            vec![
13880                "parse: bad csv",
13881                "not found: users_table",
13882                "schema: mismatch",
13883                "validation: email",
13884            ]
13885        );
13886    }
13887
13888    #[test]
13889    fn test_vm_rethrow_structured_error() {
13890        let output = run_output(
13891            r#"
13892try {
13893    try {
13894        throw DataError::NotFound("config")
13895    } catch e {
13896        throw e
13897    }
13898} catch outer {
13899    match outer {
13900        DataError::NotFound(name) => print("caught: " + name),
13901        _ => print("wrong type"),
13902    }
13903}
13904"#,
13905        );
13906        assert_eq!(output, vec!["caught: config"]);
13907    }
13908
13909    // ── Phase 28: Ownership & Move Semantics ──
13910
13911    #[test]
13912    fn test_vm_pipe_moves_value() {
13913        // x |> f() should consume x — accessing x after pipe gives error
13914        let result = run(r#"
13915fn identity(v) { v }
13916let x = [1, 2, 3]
13917x |> identity()
13918print(x)
13919"#);
13920        assert!(result.is_err());
13921        let err = result.unwrap_err().to_string();
13922        assert!(err.contains("moved"), "Error should mention 'moved': {err}");
13923    }
13924
13925    #[test]
13926    fn test_vm_clone_before_pipe() {
13927        // x.clone() |> f() should not consume x
13928        let output = run_output(
13929            r#"
13930fn identity(v) { v }
13931let x = [1, 2, 3]
13932x.clone() |> identity()
13933print(x)
13934"#,
13935        );
13936        assert_eq!(output, vec!["[1, 2, 3]"]);
13937    }
13938
13939    #[test]
13940    fn test_vm_clone_list_deep() {
13941        // Mutating a cloned list should not affect the original
13942        let output = run_output(
13943            r#"
13944let original = [1, 2, 3]
13945let copy = original.clone()
13946copy[0] = 99
13947print(original)
13948print(copy)
13949"#,
13950        );
13951        assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
13952    }
13953
13954    #[test]
13955    fn test_vm_clone_map() {
13956        let output = run_output(
13957            r#"
13958let m = map_from("a", 1, "b", 2)
13959let m2 = m.clone()
13960m2["a"] = 99
13961print(m)
13962print(m2)
13963"#,
13964        );
13965        assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
13966    }
13967
13968    #[test]
13969    fn test_vm_clone_struct() {
13970        let output = run_output(
13971            r#"
13972struct Point { x: int64, y: int64 }
13973let p = Point { x: 1, y: 2 }
13974let p2 = p.clone()
13975print(p)
13976print(p2)
13977"#,
13978        );
13979        assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
13980    }
13981
13982    #[test]
13983    fn test_vm_ref_read_only() {
13984        // &x should be readable but not mutable
13985        let result = run(r#"
13986let x = [1, 2, 3]
13987let r = &x
13988r[0] = 99
13989"#);
13990        assert!(result.is_err());
13991        let err = result.unwrap_err().to_string();
13992        assert!(
13993            err.contains("Cannot mutate a borrowed reference"),
13994            "Error should mention reference: {err}"
13995        );
13996    }
13997
13998    #[test]
13999    fn test_vm_ref_transparent_read() {
14000        // Reading through a ref should work transparently
14001        let output = run_output(
14002            r#"
14003let x = [1, 2, 3]
14004let r = &x
14005print(len(r))
14006"#,
14007        );
14008        assert_eq!(output, vec!["3"]);
14009    }
14010
14011    #[test]
14012    fn test_vm_parallel_for_basic() {
14013        // parallel for should iterate all elements (runs sequentially in VM)
14014        let output = run_output(
14015            r#"
14016let items = [10, 20, 30]
14017parallel for item in items {
14018    print(item)
14019}
14020"#,
14021        );
14022        assert_eq!(output, vec!["10", "20", "30"]);
14023    }
14024
14025    #[test]
14026    fn test_vm_moved_value_clear_error() {
14027        // Error message should mention .clone()
14028        let result = run(r#"
14029fn f(x) { x }
14030let data = "hello"
14031data |> f()
14032print(data)
14033"#);
14034        assert!(result.is_err());
14035        let err = result.unwrap_err().to_string();
14036        assert!(
14037            err.contains("clone()"),
14038            "Error should suggest .clone(): {err}"
14039        );
14040    }
14041
14042    #[test]
14043    fn test_vm_reassign_after_move() {
14044        // After moving, reassigning the variable should work
14045        let output = run_output(
14046            r#"
14047fn f(x) { x }
14048let x = 1
14049x |> f()
14050let x = 2
14051print(x)
14052"#,
14053        );
14054        assert_eq!(output, vec!["2"]);
14055    }
14056
14057    #[test]
14058    fn test_vm_pipe_chain_move() {
14059        // Chained pipes should work — intermediate values don't need explicit binding
14060        let output = run_output(
14061            r#"
14062fn double(x) { x * 2 }
14063fn add_one(x) { x + 1 }
14064let result = 5 |> double() |> add_one()
14065print(result)
14066"#,
14067        );
14068        assert_eq!(output, vec!["11"]);
14069    }
14070
14071    #[test]
14072    fn test_vm_string_clone() {
14073        // .clone() on string values
14074        let output = run_output(
14075            r#"
14076let s = "hello"
14077let s2 = s.clone()
14078print(s)
14079print(s2)
14080"#,
14081        );
14082        assert_eq!(output, vec!["hello", "hello"]);
14083    }
14084
14085    #[test]
14086    fn test_vm_ref_method_dispatch() {
14087        // Methods should be callable through references
14088        let output = run_output(
14089            r#"
14090let s = "hello world"
14091let r = &s
14092print(r.len())
14093"#,
14094        );
14095        assert_eq!(output, vec!["11"]);
14096    }
14097
14098    #[test]
14099    fn test_vm_ref_member_access() {
14100        // Member access through ref should work
14101        let output = run_output(
14102            r#"
14103struct Point { x: int64, y: int64 }
14104let p = Point { x: 10, y: 20 }
14105let r = &p
14106print(r.x)
14107"#,
14108        );
14109        assert_eq!(output, vec!["10"]);
14110    }
14111
14112    #[test]
14113    fn test_vm_ref_set_member_blocked() {
14114        // Setting a member through a ref should fail
14115        let result = run(r#"
14116struct Point { x: int64, y: int64 }
14117let p = Point { x: 10, y: 20 }
14118let r = &p
14119r.x = 99
14120"#);
14121        assert!(result.is_err());
14122        let err = result.unwrap_err().to_string();
14123        assert!(
14124            err.contains("Cannot mutate a borrowed reference"),
14125            "Error: {err}"
14126        );
14127    }
14128
14129    // ── Phase 29: IR Integration Tests ──
14130
14131    #[test]
14132    fn test_ir_filter_merge_chain() {
14133        // Two adjacent filters should be merged by the IR optimizer
14134        let dir = tempfile::tempdir().unwrap();
14135        let csv = dir.path().join("data.csv");
14136        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14137        let src = format!(
14138            r#"let t = read_csv("{}")
14139let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14140print(r)"#,
14141            csv.to_str().unwrap()
14142        );
14143        let output = run_output(&src);
14144        // Both Alice(30) and Charlie(35) pass both filters
14145        assert!(
14146            output[0].contains("Alice"),
14147            "Output should contain Alice: {}",
14148            output[0]
14149        );
14150        assert!(
14151            output[0].contains("Charlie"),
14152            "Output should contain Charlie: {}",
14153            output[0]
14154        );
14155        assert!(
14156            !output[0].contains("Bob"),
14157            "Output should not contain Bob: {}",
14158            output[0]
14159        );
14160    }
14161
14162    #[test]
14163    fn test_ir_predicate_pushdown_through_select() {
14164        // filter after select should be pushed before select by IR optimizer
14165        let dir = tempfile::tempdir().unwrap();
14166        let csv = dir.path().join("data.csv");
14167        std::fs::write(
14168            &csv,
14169            "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14170        )
14171        .unwrap();
14172        let src = format!(
14173            r#"let t = read_csv("{}")
14174let r = t |> select(name, age) |> filter(age > 25) |> collect()
14175print(r)"#,
14176            csv.to_str().unwrap()
14177        );
14178        let output = run_output(&src);
14179        assert!(output[0].contains("Alice"), "Output should contain Alice");
14180        assert!(
14181            output[0].contains("Charlie"),
14182            "Output should contain Charlie"
14183        );
14184        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14185    }
14186
14187    #[test]
14188    fn test_ir_sort_filter_pushdown() {
14189        // filter after sort should be pushed before sort
14190        let dir = tempfile::tempdir().unwrap();
14191        let csv = dir.path().join("data.csv");
14192        std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14193        let src = format!(
14194            r#"let t = read_csv("{}")
14195let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14196print(r)"#,
14197            csv.to_str().unwrap()
14198        );
14199        let output = run_output(&src);
14200        assert!(output[0].contains("Alice"), "Output should contain Alice");
14201        assert!(
14202            output[0].contains("Charlie"),
14203            "Output should contain Charlie"
14204        );
14205        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14206    }
14207
14208    #[test]
14209    fn test_ir_multi_operation_chain() {
14210        // Complex chain: filter + select + sort + limit
14211        let dir = tempfile::tempdir().unwrap();
14212        let csv = dir.path().join("data.csv");
14213        std::fs::write(
14214            &csv,
14215            "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14216        )
14217        .unwrap();
14218        let src = format!(
14219            r#"let t = read_csv("{}")
14220let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14221print(r)"#,
14222            csv.to_str().unwrap()
14223        );
14224        let output = run_output(&src);
14225        // Top 2 by age descending among age>22: Charlie(35), Alice(30)
14226        assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14227        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14228    }
14229
14230    #[test]
14231    fn test_ir_pipe_move_semantics_preserved() {
14232        // The source variable should be moved after pipe chain
14233        let dir = tempfile::tempdir().unwrap();
14234        let csv = dir.path().join("data.csv");
14235        std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14236        let src = format!(
14237            r#"let t = read_csv("{}")
14238let r = t |> filter(age > 0) |> collect()
14239print(t)"#,
14240            csv.to_str().unwrap()
14241        );
14242        let result = run(&src);
14243        assert!(result.is_err(), "Should error on use-after-move");
14244    }
14245
14246    #[test]
14247    fn test_ir_non_table_op_fallback() {
14248        // A pipe chain with a non-table op should fall back to legacy path
14249        let output = run_output(
14250            r#"
14251fn double(x) { x * 2 }
14252let result = 5 |> double()
14253print(result)
14254"#,
14255        );
14256        assert_eq!(output, vec!["10"]);
14257    }
14258
14259    #[test]
14260    fn test_ir_mixed_pipe_fallback() {
14261        // A pipe into a builtin (not a table op) should use legacy path
14262        let output = run_output(
14263            r#"
14264let result = [3, 1, 2] |> len()
14265print(result)
14266"#,
14267        );
14268        assert_eq!(output, vec!["3"]);
14269    }
14270
14271    #[test]
14272    fn test_ir_single_filter_roundtrip() {
14273        // Even a single filter goes through IR and round-trips correctly
14274        let dir = tempfile::tempdir().unwrap();
14275        let csv = dir.path().join("data.csv");
14276        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14277        let src = format!(
14278            r#"let t = read_csv("{}")
14279let r = t |> filter(age > 25) |> collect()
14280print(r)"#,
14281            csv.to_str().unwrap()
14282        );
14283        let output = run_output(&src);
14284        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14285        assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14286    }
14287
14288    // ── Phase 34: Agent Framework ──
14289
14290    #[test]
14291    fn test_vm_agent_definition() {
14292        let output = run_output(
14293            r#"
14294fn search(query) { "found: " + query }
14295agent bot {
14296    model: "gpt-4o",
14297    system: "You are helpful.",
14298    tools {
14299        search: {
14300            description: "Search the web",
14301            parameters: {}
14302        }
14303    },
14304    max_turns: 5
14305}
14306print(type_of(bot))
14307print(bot)
14308"#,
14309        );
14310        assert_eq!(output, vec!["agent", "<agent bot>"]);
14311    }
14312
14313    #[test]
14314    fn test_vm_agent_minimal() {
14315        let output = run_output(
14316            r#"
14317agent minimal_bot {
14318    model: "claude-sonnet-4-20250514"
14319}
14320print(type_of(minimal_bot))
14321"#,
14322        );
14323        assert_eq!(output, vec!["agent"]);
14324    }
14325
14326    #[test]
14327    fn test_vm_agent_with_base_url() {
14328        let output = run_output(
14329            r#"
14330agent local_bot {
14331    model: "llama3",
14332    base_url: "http://localhost:11434/v1",
14333    max_turns: 3
14334}
14335print(local_bot)
14336"#,
14337        );
14338        assert_eq!(output, vec!["<agent local_bot>"]);
14339    }
14340
14341    #[test]
14342    fn test_vm_agent_multiple_tools() {
14343        let output = run_output(
14344            r#"
14345fn search(query) { "result" }
14346fn weather(city) { "sunny" }
14347agent helper {
14348    model: "gpt-4o",
14349    tools {
14350        search: { description: "Search", parameters: {} },
14351        weather: { description: "Get weather", parameters: {} }
14352    }
14353}
14354print(type_of(helper))
14355"#,
14356        );
14357        assert_eq!(output, vec!["agent"]);
14358    }
14359
14360    #[test]
14361    fn test_vm_agent_lifecycle_hooks_stored() {
14362        let output = run_output(
14363            r#"
14364fn search(q) { "result" }
14365agent bot {
14366    model: "gpt-4o",
14367    tools {
14368        search: { description: "Search", parameters: {} }
14369    },
14370    on_tool_call {
14371        println("tool: " + tool_name)
14372    }
14373    on_complete {
14374        println("done")
14375    }
14376}
14377print(type_of(bot))
14378print(type_of(__agent_bot_on_tool_call__))
14379print(type_of(__agent_bot_on_complete__))
14380"#,
14381        );
14382        assert_eq!(output, vec!["agent", "function", "function"]);
14383    }
14384
14385    #[test]
14386    fn test_vm_agent_lifecycle_hook_callable() {
14387        let output = run_output(
14388            r#"
14389agent bot {
14390    model: "gpt-4o",
14391    on_tool_call {
14392        println("called: " + tool_name + " -> " + tool_result)
14393    }
14394    on_complete {
14395        println("completed")
14396    }
14397}
14398__agent_bot_on_tool_call__("search", "query", "found it")
14399__agent_bot_on_complete__("hello")
14400"#,
14401        );
14402        assert_eq!(output, vec!["called: search -> found it", "completed"]);
14403    }
14404}