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/// Extract the common `(table, config, table_name, [mode])` arguments shared by
49/// the REST-warehouse write builtins.
50#[cfg(all(
51    feature = "native",
52    any(feature = "snowflake", feature = "bigquery", feature = "databricks")
53))]
54fn write_args(
55    args: &[VmValue],
56    name: &str,
57) -> Result<(tl_data::DataFrame, String, String, String), TlError> {
58    if args.len() < 3 {
59        return Err(runtime_err(format!(
60            "{name}() expects (table, config, table_name, [mode])"
61        )));
62    }
63    let df = match &args[0] {
64        VmValue::Table(t) => t.df.clone(),
65        _ => return Err(runtime_err(format!("{name}() first arg must be a table"))),
66    };
67    let cfg = match &args[1] {
68        VmValue::String(s) => resolve_tl_config_connection(s),
69        _ => return Err(runtime_err(format!("{name}() config must be a string"))),
70    };
71    let table_name = match &args[2] {
72        VmValue::String(s) => s.to_string(),
73        _ => return Err(runtime_err(format!("{name}() table_name must be a string"))),
74    };
75    let mode = match args.get(3) {
76        None | Some(VmValue::None) => "create".to_string(),
77        Some(VmValue::String(s)) => s.to_string(),
78        _ => return Err(runtime_err(format!("{name}() mode must be a string"))),
79    };
80    Ok((df, cfg, table_name, mode))
81}
82
83/// Resolve a connection name via TL_CONFIG_PATH config file.
84/// If `name` looks like a connection string (contains `=` or `://`), return it as-is.
85/// Otherwise, look it up in the JSON config file at `TL_CONFIG_PATH` (or `./tl_config.json`).
86fn resolve_tl_config_connection(name: &str) -> String {
87    // If it already looks like a connection string, pass through
88    if name.contains('=') || name.contains("://") {
89        return name.to_string();
90    }
91    // Try to load config
92    let config_path =
93        std::env::var("TL_CONFIG_PATH").unwrap_or_else(|_| "tl_config.json".to_string());
94    let Ok(contents) = std::fs::read_to_string(&config_path) else {
95        return name.to_string();
96    };
97    let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
98        return name.to_string();
99    };
100    // Look up in "connections" object first, then top-level
101    if let Some(conn) = json
102        .get("connections")
103        .and_then(|c| c.get(name))
104        .and_then(|v| v.as_str())
105    {
106        return conn.to_string();
107    }
108    if let Some(conn) = json.get(name).and_then(|v| v.as_str()) {
109        return conn.to_string();
110    }
111    // Not found — return original (will fail at connection time with a clear error)
112    name.to_string()
113}
114
115/// Compare two VmValues for equality (used by set operations).
116fn vm_values_equal(a: &VmValue, b: &VmValue) -> bool {
117    match (a, b) {
118        (VmValue::Int(x), VmValue::Int(y)) => x == y,
119        (VmValue::Float(x), VmValue::Float(y)) => x == y,
120        (VmValue::String(x), VmValue::String(y)) => x == y,
121        (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
122        (VmValue::None, VmValue::None) => true,
123        _ => false,
124    }
125}
126
127#[cfg(feature = "native")]
128/// Resolve a file path within a package directory for package imports.
129/// `pkg_root` is the package root (containing tl.toml).
130/// `remaining` are the path segments after the package name.
131/// Entry point convention: src/lib.tl > src/mod.tl > src/main.tl > mod.tl > lib.tl
132fn resolve_package_file(pkg_root: &std::path::Path, remaining: &[&str]) -> Option<String> {
133    if remaining.is_empty() {
134        // Import the package itself — find entry point
135        let src = pkg_root.join("src");
136        for entry in &["lib.tl", "mod.tl", "main.tl"] {
137            let p = src.join(entry);
138            if p.exists() {
139                return Some(p.to_string_lossy().to_string());
140            }
141        }
142        for entry in &["mod.tl", "lib.tl"] {
143            let p = pkg_root.join(entry);
144            if p.exists() {
145                return Some(p.to_string_lossy().to_string());
146            }
147        }
148        return None;
149    }
150
151    // Try src/<remaining>.tl, then src/<remaining>/mod.tl
152    let rel = remaining.join("/");
153    let src = pkg_root.join("src");
154
155    let file_path = src.join(format!("{rel}.tl"));
156    if file_path.exists() {
157        return Some(file_path.to_string_lossy().to_string());
158    }
159
160    let dir_path = src.join(&rel).join("mod.tl");
161    if dir_path.exists() {
162        return Some(dir_path.to_string_lossy().to_string());
163    }
164
165    // Also try without src/ prefix
166    let file_path = pkg_root.join(format!("{rel}.tl"));
167    if file_path.exists() {
168        return Some(file_path.to_string_lossy().to_string());
169    }
170
171    let dir_path = pkg_root.join(&rel).join("mod.tl");
172    if dir_path.exists() {
173        return Some(dir_path.to_string_lossy().to_string());
174    }
175
176    // Parent fallback for item-within-module
177    if remaining.len() > 1 {
178        let parent = &remaining[..remaining.len() - 1];
179        let parent_rel = parent.join("/");
180        let parent_file = src.join(format!("{parent_rel}.tl"));
181        if parent_file.exists() {
182            return Some(parent_file.to_string_lossy().to_string());
183        }
184        let parent_file = pkg_root.join(format!("{parent_rel}.tl"));
185        if parent_file.exists() {
186            return Some(parent_file.to_string_lossy().to_string());
187        }
188    }
189
190    None
191}
192
193/// Convert serde_json::Value to VmValue
194fn vm_json_to_value(v: &serde_json::Value) -> VmValue {
195    match v {
196        serde_json::Value::Null => VmValue::None,
197        serde_json::Value::Bool(b) => VmValue::Bool(*b),
198        serde_json::Value::Number(n) => {
199            if let Some(i) = n.as_i64() {
200                VmValue::Int(i)
201            } else {
202                VmValue::Float(n.as_f64().unwrap_or(0.0))
203            }
204        }
205        serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
206        serde_json::Value::Array(arr) => {
207            VmValue::List(Box::new(arr.iter().map(vm_json_to_value).collect()))
208        }
209        serde_json::Value::Object(obj) => VmValue::Map(Box::new(
210            obj.iter()
211                .map(|(k, v)| (Arc::from(k.as_str()), vm_json_to_value(v)))
212                .collect(),
213        )),
214    }
215}
216
217/// Convert VmValue to serde_json::Value
218fn vm_value_to_json(v: &VmValue) -> serde_json::Value {
219    match v {
220        VmValue::None => serde_json::Value::Null,
221        VmValue::Bool(b) => serde_json::Value::Bool(*b),
222        VmValue::Int(n) => serde_json::json!(*n),
223        VmValue::Float(n) => serde_json::json!(*n),
224        VmValue::String(s) => serde_json::Value::String(s.to_string()),
225        VmValue::List(items) => {
226            serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
227        }
228        VmValue::Map(pairs) => {
229            let obj: serde_json::Map<String, serde_json::Value> = pairs
230                .iter()
231                .map(|(k, v)| (k.to_string(), vm_value_to_json(v)))
232                .collect();
233            serde_json::Value::Object(obj)
234        }
235        VmValue::Secret(_) => serde_json::Value::String("***".to_string()),
236        _ => serde_json::Value::String(format!("{v}")),
237    }
238}
239
240/// Minimum list size before we attempt parallel execution.
241#[cfg(feature = "native")]
242const PARALLEL_THRESHOLD: usize = 10_000;
243
244/// Check if a closure is pure (no captured upvalues) and thus safe to run in parallel.
245#[cfg(feature = "native")]
246fn is_pure_closure(func: &VmValue) -> bool {
247    match func {
248        VmValue::Function(closure) => closure.upvalues.is_empty(),
249        _ => false,
250    }
251}
252
253/// Execute a pure function (no upvalues) in an isolated mini-VM.
254/// Used by rayon parallel operations — each thread gets its own stack.
255#[cfg(feature = "native")]
256fn execute_pure_fn(proto: &Arc<Prototype>, args: &[VmValue]) -> Result<VmValue, TlError> {
257    let base = 0;
258    let num_regs = proto.num_registers as usize;
259    let mut stack = vec![VmValue::None; num_regs + 1];
260    for (i, arg) in args.iter().enumerate() {
261        stack[i] = arg.clone();
262    }
263
264    let mut ip = 0;
265    loop {
266        if ip >= proto.code.len() {
267            return Ok(VmValue::None);
268        }
269        let inst = proto.code[ip];
270        let op = decode_op(inst);
271        let a = decode_a(inst);
272        let b = decode_b(inst);
273        let c = decode_c(inst);
274        let bx = decode_bx(inst);
275        let sbx = decode_sbx(inst);
276
277        ip += 1;
278
279        match op {
280            Op::LoadConst => {
281                let val = match &proto.constants[bx as usize] {
282                    Constant::Int(n) => VmValue::Int(*n),
283                    Constant::Float(n) => VmValue::Float(*n),
284                    Constant::String(s) => VmValue::String(s.clone()),
285                    Constant::Decimal(s) => {
286                        use std::str::FromStr;
287                        VmValue::Decimal(rust_decimal::Decimal::from_str(s).unwrap_or_default())
288                    }
289                    _ => VmValue::None,
290                };
291                stack[base + a as usize] = val;
292            }
293            Op::LoadNone => stack[base + a as usize] = VmValue::None,
294            Op::LoadTrue => stack[base + a as usize] = VmValue::Bool(true),
295            Op::LoadFalse => stack[base + a as usize] = VmValue::Bool(false),
296            Op::Move | Op::GetLocal => {
297                let val = stack[base + b as usize].clone();
298                stack[base + a as usize] = val;
299            }
300            Op::SetLocal => {
301                let val = stack[base + a as usize].clone();
302                stack[base + b as usize] = val;
303            }
304            Op::Add => {
305                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
306                    (VmValue::Int(x), VmValue::Int(y)) => x
307                        .checked_add(*y)
308                        .map(VmValue::Int)
309                        .unwrap_or_else(|| VmValue::Float(*x as f64 + *y as f64)),
310                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x + y),
311                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 + y),
312                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x + *y as f64),
313                    _ => return Err(runtime_err("Cannot add in parallel fn")),
314                };
315                stack[base + a as usize] = result;
316            }
317            Op::Sub => {
318                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
319                    (VmValue::Int(x), VmValue::Int(y)) => x
320                        .checked_sub(*y)
321                        .map(VmValue::Int)
322                        .unwrap_or_else(|| VmValue::Float(*x as f64 - *y as f64)),
323                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x - y),
324                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 - y),
325                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x - *y as f64),
326                    _ => return Err(runtime_err("Cannot subtract in parallel fn")),
327                };
328                stack[base + a as usize] = result;
329            }
330            Op::Mul => {
331                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
332                    (VmValue::Int(x), VmValue::Int(y)) => x
333                        .checked_mul(*y)
334                        .map(VmValue::Int)
335                        .unwrap_or_else(|| VmValue::Float(*x as f64 * *y as f64)),
336                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x * y),
337                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 * y),
338                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x * *y as f64),
339                    _ => return Err(runtime_err("Cannot multiply in parallel fn")),
340                };
341                stack[base + a as usize] = result;
342            }
343            Op::Div => {
344                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
345                    (VmValue::Int(x), VmValue::Int(y)) => {
346                        if *y == 0 {
347                            return Err(runtime_err("Division by zero"));
348                        }
349                        VmValue::Int(x / y)
350                    }
351                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x / y),
352                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 / y),
353                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x / *y as f64),
354                    _ => return Err(runtime_err("Cannot divide in parallel fn")),
355                };
356                stack[base + a as usize] = result;
357            }
358            Op::Mod => {
359                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
360                    (VmValue::Int(x), VmValue::Int(y)) => {
361                        if *y == 0 {
362                            return Err(runtime_err("Modulo by zero"));
363                        }
364                        VmValue::Int(x % y)
365                    }
366                    (VmValue::Float(x), VmValue::Float(y)) => {
367                        if *y == 0.0 {
368                            return Err(runtime_err("Modulo by zero"));
369                        }
370                        VmValue::Float(x % y)
371                    }
372                    _ => return Err(runtime_err("Cannot modulo in parallel fn")),
373                };
374                stack[base + a as usize] = result;
375            }
376            Op::Pow => {
377                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
378                    (VmValue::Int(x), VmValue::Int(y)) => {
379                        VmValue::Int((*x as f64).powi(*y as i32) as i64)
380                    }
381                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x.powf(*y)),
382                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float((*x as f64).powf(*y)),
383                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x.powi(*y as i32)),
384                    _ => return Err(runtime_err("Cannot pow in parallel fn")),
385                };
386                stack[base + a as usize] = result;
387            }
388            Op::Neg => {
389                let result = match &stack[base + b as usize] {
390                    VmValue::Int(n) => VmValue::Int(-n),
391                    VmValue::Float(n) => VmValue::Float(-n),
392                    _ => return Err(runtime_err("Cannot negate in parallel fn")),
393                };
394                stack[base + a as usize] = result;
395            }
396            Op::Eq => {
397                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
398                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
399                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
400                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
401                    (VmValue::String(x), VmValue::String(y)) => x == y,
402                    (VmValue::None, VmValue::None) => true,
403                    _ => false,
404                };
405                stack[base + a as usize] = VmValue::Bool(eq);
406            }
407            Op::Neq => {
408                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
409                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
410                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
411                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
412                    (VmValue::String(x), VmValue::String(y)) => x == y,
413                    (VmValue::None, VmValue::None) => true,
414                    _ => false,
415                };
416                stack[base + a as usize] = VmValue::Bool(!eq);
417            }
418            Op::Lt | Op::Gt | Op::Lte | Op::Gte => {
419                let cmp = match (&stack[base + b as usize], &stack[base + c as usize]) {
420                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y) as i8,
421                    (VmValue::Float(x), VmValue::Float(y)) => {
422                        if x < y {
423                            -1
424                        } else if x > y {
425                            1
426                        } else {
427                            0
428                        }
429                    }
430                    _ => return Err(runtime_err("Cannot compare in parallel fn")),
431                };
432                let result = match op {
433                    Op::Lt => cmp < 0,
434                    Op::Gt => cmp > 0,
435                    Op::Lte => cmp <= 0,
436                    Op::Gte => cmp >= 0,
437                    _ => unreachable!(),
438                };
439                stack[base + a as usize] = VmValue::Bool(result);
440            }
441            Op::And => {
442                let left = stack[base + b as usize].is_truthy();
443                let right = stack[base + c as usize].is_truthy();
444                stack[base + a as usize] = VmValue::Bool(left && right);
445            }
446            Op::Or => {
447                let left = stack[base + b as usize].is_truthy();
448                let right = stack[base + c as usize].is_truthy();
449                stack[base + a as usize] = VmValue::Bool(left || right);
450            }
451            Op::Not => {
452                let val = !stack[base + b as usize].is_truthy();
453                stack[base + a as usize] = VmValue::Bool(val);
454            }
455            Op::Jump => {
456                ip = (ip as i32 + sbx as i32) as usize;
457            }
458            Op::JumpIfFalse => {
459                if !stack[base + a as usize].is_truthy() {
460                    ip = (ip as i32 + sbx as i32) as usize;
461                }
462            }
463            Op::JumpIfTrue => {
464                if stack[base + a as usize].is_truthy() {
465                    ip = (ip as i32 + sbx as i32) as usize;
466                }
467            }
468            Op::Return => {
469                return Ok(stack[base + a as usize].clone());
470            }
471            // Unsupported ops in parallel context — fall back silently
472            _ => return Err(runtime_err("Unsupported op in parallel function")),
473        }
474    }
475}
476
477/// A call frame on the VM stack.
478struct CallFrame {
479    prototype: Arc<Prototype>,
480    ip: usize,
481    base: usize,
482    upvalues: Vec<UpvalueRef>,
483}
484
485/// A try-catch handler on the handler stack.
486struct TryHandler {
487    /// Frame index where try was entered
488    frame_idx: usize,
489    /// IP to jump to (catch handler)
490    catch_ip: usize,
491}
492
493/// The bytecode virtual machine.
494pub struct Vm {
495    /// Register stack
496    pub stack: Vec<VmValue>,
497    /// Call frame stack
498    frames: Vec<CallFrame>,
499    /// Global variables
500    pub globals: HashMap<String, VmValue>,
501    /// Data engine (lazily initialized)
502    #[cfg(feature = "native")]
503    data_engine: Option<DataEngine>,
504    /// Captured output (for testing)
505    pub output: Vec<String>,
506    /// Try-catch handler stack
507    try_handlers: Vec<TryHandler>,
508    /// Yielded value (Some when Op::Yield suspends a generator)
509    yielded_value: Option<VmValue>,
510    /// IP at the point of yield (instruction after the Yield op)
511    yielded_ip: usize,
512    /// Current file path (for relative imports)
513    pub file_path: Option<String>,
514    /// Module cache: resolved path → exports
515    module_cache: HashMap<String, HashMap<String, VmValue>>,
516    /// Files currently being imported (circular detection)
517    importing_files: std::collections::HashSet<String>,
518    /// Tracks which globals are public (for module export filtering)
519    pub public_items: std::collections::HashSet<String>,
520    /// Package roots: package_name → source directory
521    pub package_roots: HashMap<String, std::path::PathBuf>,
522    /// Project root (where tl.toml lives)
523    pub project_root: Option<std::path::PathBuf>,
524    /// Schema registry for versioned schemas
525    pub schema_registry: crate::schema::SchemaRegistry,
526    /// Secret vault for credential management (zeroed on drop)
527    pub secret_vault: SecretVault,
528    /// Security policy (optional, set via --sandbox)
529    pub security_policy: Option<crate::security::SecurityPolicy>,
530    /// Tokio runtime for async builtins (lazily initialized)
531    #[cfg(feature = "async-runtime")]
532    runtime: Option<Arc<tokio::runtime::Runtime>>,
533    /// Stashed thrown value for structured error preservation in try/catch
534    thrown_value: Option<VmValue>,
535    /// GPU operations dispatcher (lazily initialized)
536    #[cfg(feature = "gpu")]
537    gpu_ops: Option<tl_gpu::GpuOps>,
538    /// MCP clients associated with agents (agent_name -> clients)
539    #[cfg(feature = "mcp")]
540    mcp_agent_clients: HashMap<String, Vec<Arc<tl_mcp::McpClient>>>,
541}
542
543/// A secret vault that zeros entries on drop to reduce credential exposure in memory.
544#[derive(Debug, Clone, Default)]
545pub struct SecretVault(HashMap<String, String>);
546
547impl SecretVault {
548    pub fn new() -> Self {
549        Self(HashMap::new())
550    }
551    pub fn get(&self, key: &str) -> Option<&String> {
552        self.0.get(key)
553    }
554    pub fn insert(&mut self, key: String, val: String) {
555        self.0.insert(key, val);
556    }
557    pub fn remove(&mut self, key: &str) {
558        self.0.remove(key);
559    }
560    pub fn keys(&self) -> impl Iterator<Item = &String> {
561        self.0.keys()
562    }
563}
564
565impl Drop for SecretVault {
566    fn drop(&mut self) {
567        for val in self.0.values_mut() {
568            // Overwrite the string's buffer with zeros before deallocation.
569            // SAFETY: we write zeros into the valid allocated range of the String.
570            unsafe {
571                let ptr = val.as_mut_vec().as_mut_ptr();
572                std::ptr::write_bytes(ptr, 0, val.len());
573            }
574        }
575        self.0.clear();
576    }
577}
578
579impl Vm {
580    pub fn new() -> Self {
581        let mut vm = Vm {
582            stack: Vec::with_capacity(256),
583            frames: Vec::new(),
584            globals: HashMap::new(),
585            #[cfg(feature = "native")]
586            data_engine: None,
587            output: Vec::new(),
588            try_handlers: Vec::new(),
589            yielded_value: None,
590            yielded_ip: 0,
591            file_path: None,
592            module_cache: HashMap::new(),
593            importing_files: std::collections::HashSet::new(),
594            public_items: std::collections::HashSet::new(),
595            package_roots: HashMap::new(),
596            project_root: None,
597            schema_registry: crate::schema::SchemaRegistry::new(),
598            secret_vault: SecretVault::new(),
599            security_policy: None,
600            #[cfg(feature = "async-runtime")]
601            runtime: None,
602            thrown_value: None,
603            #[cfg(feature = "gpu")]
604            gpu_ops: None,
605            #[cfg(feature = "mcp")]
606            mcp_agent_clients: HashMap::new(),
607        };
608        // Phase 27: Register built-in error enum definitions
609        vm.globals.insert(
610            "DataError".into(),
611            VmValue::EnumDef(Arc::new(VmEnumDef {
612                name: Arc::from("DataError"),
613                variants: vec![
614                    (Arc::from("ParseError"), 2),
615                    (Arc::from("SchemaError"), 3),
616                    (Arc::from("ValidationError"), 2),
617                    (Arc::from("NotFound"), 1),
618                ],
619            })),
620        );
621        vm.globals.insert(
622            "NetworkError".into(),
623            VmValue::EnumDef(Arc::new(VmEnumDef {
624                name: Arc::from("NetworkError"),
625                variants: vec![
626                    (Arc::from("ConnectionError"), 2),
627                    (Arc::from("TimeoutError"), 1),
628                    (Arc::from("HttpError"), 2),
629                ],
630            })),
631        );
632        vm.globals.insert(
633            "ConnectorError".into(),
634            VmValue::EnumDef(Arc::new(VmEnumDef {
635                name: Arc::from("ConnectorError"),
636                variants: vec![
637                    (Arc::from("AuthError"), 2),
638                    (Arc::from("QueryError"), 2),
639                    (Arc::from("ConfigError"), 2),
640                ],
641            })),
642        );
643        // Phase 3: Register MCP builtins as globals
644        #[cfg(feature = "mcp")]
645        {
646            vm.globals.insert(
647                "mcp_connect".to_string(),
648                VmValue::Builtin(BuiltinId::McpConnect),
649            );
650            vm.globals.insert(
651                "mcp_list_tools".to_string(),
652                VmValue::Builtin(BuiltinId::McpListTools),
653            );
654            vm.globals.insert(
655                "mcp_call_tool".to_string(),
656                VmValue::Builtin(BuiltinId::McpCallTool),
657            );
658            vm.globals.insert(
659                "mcp_disconnect".to_string(),
660                VmValue::Builtin(BuiltinId::McpDisconnect),
661            );
662            vm.globals.insert(
663                "mcp_serve".to_string(),
664                VmValue::Builtin(BuiltinId::McpServe),
665            );
666            vm.globals.insert(
667                "mcp_server_info".to_string(),
668                VmValue::Builtin(BuiltinId::McpServerInfo),
669            );
670            vm.globals
671                .insert("mcp_ping".to_string(), VmValue::Builtin(BuiltinId::McpPing));
672            vm.globals.insert(
673                "mcp_list_resources".to_string(),
674                VmValue::Builtin(BuiltinId::McpListResources),
675            );
676            vm.globals.insert(
677                "mcp_read_resource".to_string(),
678                VmValue::Builtin(BuiltinId::McpReadResource),
679            );
680            vm.globals.insert(
681                "mcp_list_prompts".to_string(),
682                VmValue::Builtin(BuiltinId::McpListPrompts),
683            );
684            vm.globals.insert(
685                "mcp_get_prompt".to_string(),
686                VmValue::Builtin(BuiltinId::McpGetPrompt),
687            );
688        }
689        vm
690    }
691
692    /// Lazily initialize and return the tokio runtime.
693    #[cfg(feature = "async-runtime")]
694    fn ensure_runtime(&mut self) -> Arc<tokio::runtime::Runtime> {
695        if self.runtime.is_none() {
696            self.runtime = Some(Arc::new(
697                tokio::runtime::Builder::new_multi_thread()
698                    .enable_all()
699                    .build()
700                    .expect("Failed to create tokio runtime"),
701            ));
702        }
703        self.runtime.as_ref().unwrap().clone()
704    }
705
706    /// Lazily initialize and return the GPU ops dispatcher.
707    #[cfg(feature = "gpu")]
708    fn get_gpu_ops(&mut self) -> Result<&tl_gpu::GpuOps, TlError> {
709        if self.gpu_ops.is_none() {
710            let device =
711                tl_gpu::GpuDevice::get().ok_or_else(|| runtime_err("No GPU device available"))?;
712            self.gpu_ops = Some(tl_gpu::GpuOps::new(device));
713        }
714        Ok(self.gpu_ops.as_ref().unwrap())
715    }
716
717    /// Extract a GpuTensor from a VmValue, auto-uploading CPU tensors if needed.
718    #[cfg(feature = "gpu")]
719    fn ensure_gpu_tensor(&mut self, val: &VmValue) -> Result<Arc<tl_gpu::GpuTensor>, TlError> {
720        match val {
721            VmValue::GpuTensor(gt) => Ok(gt.clone()),
722            #[cfg(feature = "native")]
723            VmValue::Tensor(t) => {
724                let device = tl_gpu::GpuDevice::get()
725                    .ok_or_else(|| runtime_err("No GPU device available"))?;
726                Ok(Arc::new(tl_gpu::GpuTensor::from_cpu(t, device)))
727            }
728            _ => Err(runtime_err(format!(
729                "Expected tensor or gpu_tensor, got {}",
730                val.type_name()
731            ))),
732        }
733    }
734
735    #[cfg(feature = "native")]
736    fn engine(&mut self) -> &DataEngine {
737        if self.data_engine.is_none() {
738            self.data_engine = Some(DataEngine::new());
739        }
740        self.data_engine.as_ref().unwrap()
741    }
742
743    /// Ensure the stack has at least `size` slots.
744    fn ensure_stack(&mut self, size: usize) {
745        if self.stack.len() < size {
746            self.stack.resize(size, VmValue::None);
747        }
748    }
749
750    /// Execute a compiled prototype.
751    pub fn execute(&mut self, proto: &Prototype) -> Result<VmValue, TlError> {
752        let proto = Arc::new(proto.clone());
753        let base = self.stack.len();
754        self.ensure_stack(base + proto.num_registers as usize + 1);
755
756        self.frames.push(CallFrame {
757            prototype: proto,
758            ip: 0,
759            base,
760            upvalues: Vec::new(),
761        });
762
763        self.run().map_err(|e| self.enrich_error(e))
764    }
765
766    // -- Debug API (Phase H5) --
767
768    /// Prepare the VM for debug execution by pushing a call frame without running.
769    pub fn debug_load(&mut self, proto: &Prototype) {
770        let proto = Arc::new(proto.clone());
771        let base = self.stack.len();
772        self.ensure_stack(base + proto.num_registers as usize + 1);
773        self.frames.push(CallFrame {
774            prototype: proto,
775            ip: 0,
776            base,
777            upvalues: Vec::new(),
778        });
779    }
780
781    /// Execute a single instruction in debug mode. Returns:
782    /// - Ok(None) → instruction executed, more to go
783    /// - Ok(Some(val)) → execution completed with return value
784    /// - Err → runtime error
785    pub fn debug_step(&mut self) -> Result<Option<VmValue>, TlError> {
786        let entry_depth = 1; // Always run at top level depth
787        self.run_step(entry_depth).map_err(|e| self.enrich_error(e))
788    }
789
790    /// Get the current source line number (1-based) or 0 if unknown.
791    pub fn debug_current_line(&self) -> u32 {
792        if let Some(frame) = self.frames.last() {
793            let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
794            if ip < frame.prototype.lines.len() {
795                frame.prototype.lines[ip]
796            } else {
797                0
798            }
799        } else {
800            0
801        }
802    }
803
804    /// Get the current function name being executed.
805    pub fn debug_current_function(&self) -> String {
806        self.frames
807            .last()
808            .map(|f| f.prototype.name.clone())
809            .unwrap_or_default()
810    }
811
812    /// Check if the VM has finished executing (no more frames).
813    pub fn debug_is_done(&self) -> bool {
814        self.frames.is_empty()
815            || self
816                .frames
817                .last()
818                .is_some_and(|f| f.ip >= f.prototype.code.len())
819    }
820
821    /// Get a global variable by name.
822    pub fn debug_get_global(&self, name: &str) -> Option<&VmValue> {
823        self.globals.get(name)
824    }
825
826    /// Get a local variable by name (looks in top_level_locals of current frame).
827    pub fn debug_get_local(&self, name: &str) -> Option<&VmValue> {
828        if let Some(frame) = self.frames.last() {
829            for (local_name, reg) in &frame.prototype.top_level_locals {
830                if local_name == name {
831                    let idx = frame.base + *reg as usize;
832                    if idx < self.stack.len() {
833                        return Some(&self.stack[idx]);
834                    }
835                }
836            }
837        }
838        None
839    }
840
841    /// Get all local variables in the current frame.
842    pub fn debug_locals(&self) -> Vec<(String, &VmValue)> {
843        let mut result = Vec::new();
844        if let Some(frame) = self.frames.last() {
845            for (name, reg) in &frame.prototype.top_level_locals {
846                let idx = frame.base + *reg as usize;
847                if idx < self.stack.len() {
848                    result.push((name.clone(), &self.stack[idx]));
849                }
850            }
851        }
852        result
853    }
854
855    /// Get the current IP (instruction pointer).
856    pub fn debug_current_ip(&self) -> usize {
857        self.frames.last().map(|f| f.ip).unwrap_or(0)
858    }
859
860    /// Run until the next source line changes (step over).
861    pub fn debug_step_line(&mut self) -> Result<Option<VmValue>, TlError> {
862        let start_line = self.debug_current_line();
863        loop {
864            if self.debug_is_done() {
865                return Ok(Some(VmValue::None));
866            }
867            let result = self.debug_step()?;
868            if result.is_some() {
869                return Ok(result);
870            }
871            let new_line = self.debug_current_line();
872            if new_line != start_line && new_line != 0 {
873                return Ok(None);
874            }
875        }
876    }
877
878    /// Continue execution until a breakpoint line is hit or execution completes.
879    pub fn debug_continue(&mut self, breakpoints: &[u32]) -> Result<Option<VmValue>, TlError> {
880        loop {
881            if self.debug_is_done() {
882                return Ok(Some(VmValue::None));
883            }
884            let result = self.debug_step()?;
885            if result.is_some() {
886                return Ok(result);
887            }
888            let line = self.debug_current_line();
889            if breakpoints.contains(&line) {
890                return Ok(None);
891            }
892        }
893    }
894
895    /// Enrich a runtime error with line number and stack trace from the current call frames.
896    fn enrich_error(&self, err: TlError) -> TlError {
897        match err {
898            TlError::Runtime(mut re) => {
899                // Build stack trace from remaining frames
900                let mut trace = Vec::new();
901                for frame in self.frames.iter().rev() {
902                    let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
903                    let line = if ip < frame.prototype.lines.len() {
904                        frame.prototype.lines[ip]
905                    } else {
906                        0
907                    };
908                    trace.push(tl_errors::StackFrame {
909                        function: frame.prototype.name.clone(),
910                        line,
911                    });
912                }
913                // Set span from the innermost frame's line if not already set
914                if re.span.is_none() && !trace.is_empty() && trace[0].line > 0 {
915                    // We only have line number, not byte offset, so we can't set a precise span.
916                    // But we can set a line-based marker that report_runtime_error can use.
917                    // For now, leave span as None and rely on the stack trace.
918                }
919                re.stack_trace = trace;
920                TlError::Runtime(re)
921            }
922            other => other,
923        }
924    }
925
926    /// Main dispatch loop. Runs the current (topmost) frame until Return.
927    fn run(&mut self) -> Result<VmValue, TlError> {
928        let entry_depth = self.frames.len();
929        loop {
930            let step_result = self.run_step(entry_depth);
931            match step_result {
932                Ok(Some(val)) => return Ok(val), // Return instruction
933                Ok(None) => continue,            // Normal instruction
934                Err(e) => {
935                    // Check for try handler
936                    if let Some(handler) = self.try_handlers.pop() {
937                        // Restore to handler's frame
938                        while self.frames.len() > handler.frame_idx {
939                            self.frames.pop();
940                        }
941                        if self.frames.is_empty() {
942                            return Err(e);
943                        }
944                        let fidx = self.frames.len() - 1;
945                        self.frames[fidx].ip = handler.catch_ip;
946                        let err_msg = match &e {
947                            TlError::Runtime(re) => re.message.clone(),
948                            other => format!("{other}"),
949                        };
950                        // Put error value in catch scope's first local
951                        // The compiler emits LoadNone for the catch var at catch_ip; we need to
952                        // identify the register, set the error value, and skip past the LoadNone
953                        let catch_val = self
954                            .thrown_value
955                            .take()
956                            .unwrap_or_else(|| VmValue::String(Arc::from(err_msg.as_str())));
957                        let cbase = self.frames[fidx].base;
958                        let current_ip = self.frames[fidx].ip;
959                        if current_ip < self.frames[fidx].prototype.code.len() {
960                            let catch_inst = self.frames[fidx].prototype.code[current_ip];
961                            let catch_op = decode_op(catch_inst);
962                            let catch_reg = decode_a(catch_inst);
963                            if matches!(catch_op, Op::LoadNone) {
964                                // Skip the LoadNone and write error value directly
965                                self.frames[fidx].ip += 1;
966                                self.ensure_stack(cbase + catch_reg as usize + 1);
967                                self.stack[cbase + catch_reg as usize] = catch_val;
968                            }
969                        }
970                        continue;
971                    }
972                    return Err(e);
973                }
974            }
975        }
976    }
977
978    /// Execute a single instruction. Returns Ok(Some(val)) for Return, Ok(None) for continue, Err for errors.
979    fn run_step(&mut self, entry_depth: usize) -> Result<Option<VmValue>, TlError> {
980        if self.frames.len() < entry_depth || self.frames.is_empty() {
981            return Ok(Some(VmValue::None));
982        }
983        let frame_idx = self.frames.len() - 1;
984        let frame = &self.frames[frame_idx];
985
986        if frame.ip >= frame.prototype.code.len() {
987            // End of bytecode — return None
988            self.frames.pop();
989            return Ok(Some(VmValue::None));
990        }
991
992        let inst = frame.prototype.code[frame.ip];
993        let op = decode_op(inst);
994        let a = decode_a(inst);
995        let b = decode_b(inst);
996        let c = decode_c(inst);
997        let bx = decode_bx(inst);
998        let sbx = decode_sbx(inst);
999        let base = frame.base;
1000
1001        // Advance IP before executing (some ops modify it)
1002        self.frames[frame_idx].ip += 1;
1003
1004        match op {
1005            Op::LoadConst => {
1006                let val = self.load_constant(frame_idx, bx)?;
1007                self.stack[base + a as usize] = val;
1008            }
1009            Op::LoadNone => {
1010                self.stack[base + a as usize] = VmValue::None;
1011            }
1012            Op::LoadTrue => {
1013                self.stack[base + a as usize] = VmValue::Bool(true);
1014            }
1015            Op::LoadFalse => {
1016                self.stack[base + a as usize] = VmValue::Bool(false);
1017            }
1018            Op::Move => {
1019                let val = &self.stack[base + b as usize];
1020                if matches!(val, VmValue::Moved) {
1021                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
1022                }
1023                self.stack[base + a as usize] = val.clone();
1024            }
1025            Op::GetLocal => {
1026                let val = &self.stack[base + b as usize];
1027                if matches!(val, VmValue::Moved) {
1028                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
1029                }
1030                self.stack[base + a as usize] = val.clone();
1031            }
1032            Op::SetLocal => {
1033                let val = self.stack[base + a as usize].clone();
1034                self.stack[base + b as usize] = val;
1035            }
1036            Op::GetGlobal => {
1037                let name = self.get_string_constant(frame_idx, bx)?;
1038                let val = self
1039                    .globals
1040                    .get(name.as_ref())
1041                    .cloned()
1042                    .unwrap_or(VmValue::None);
1043                if matches!(val, VmValue::Moved) {
1044                    return Err(runtime_err(format!(
1045                        "Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."
1046                    )));
1047                }
1048                self.stack[base + a as usize] = val;
1049            }
1050            Op::SetGlobal => {
1051                let name = self.get_string_constant(frame_idx, bx)?;
1052                let val = self.stack[base + a as usize].clone();
1053                // Phase 21: Detect __schema__ and __migrate__ globals and register in schema_registry
1054                #[cfg(feature = "native")]
1055                if let VmValue::String(ref s) = val {
1056                    if s.starts_with("__schema__:") {
1057                        self.process_schema_global(s);
1058                    } else if s.starts_with("__migrate__:") {
1059                        self.process_migrate_global(s);
1060                    }
1061                }
1062                self.globals.insert(name.to_string(), val);
1063            }
1064            Op::GetUpvalue => {
1065                let val = {
1066                    let frame = &self.frames[frame_idx];
1067                    match &frame.upvalues[b as usize] {
1068                        UpvalueRef::Open { stack_index } => self.stack[*stack_index].clone(),
1069                        UpvalueRef::Closed(v) => v.clone(),
1070                    }
1071                };
1072                self.stack[base + a as usize] = val;
1073            }
1074            Op::SetUpvalue => {
1075                let val = self.stack[base + a as usize].clone();
1076                let frame = &mut self.frames[frame_idx];
1077                match &mut frame.upvalues[b as usize] {
1078                    UpvalueRef::Open { stack_index } => {
1079                        let idx = *stack_index;
1080                        self.stack[idx] = val;
1081                    }
1082                    UpvalueRef::Closed(v) => {
1083                        *v = val;
1084                    }
1085                }
1086            }
1087            Op::Add => {
1088                let result = self.vm_add(base, b, c)?;
1089                self.stack[base + a as usize] = result;
1090            }
1091            Op::Sub => {
1092                let result = self.vm_sub(base, b, c)?;
1093                self.stack[base + a as usize] = result;
1094            }
1095            Op::Mul => {
1096                let result = self.vm_mul(base, b, c)?;
1097                self.stack[base + a as usize] = result;
1098            }
1099            Op::Div => {
1100                let result = self.vm_div(base, b, c)?;
1101                self.stack[base + a as usize] = result;
1102            }
1103            Op::Mod => {
1104                let result = self.vm_mod(base, b, c)?;
1105                self.stack[base + a as usize] = result;
1106            }
1107            Op::Pow => {
1108                let result = self.vm_pow(base, b, c)?;
1109                self.stack[base + a as usize] = result;
1110            }
1111            Op::Neg => {
1112                let result = match &self.stack[base + b as usize] {
1113                    VmValue::Int(n) => VmValue::Int(-n),
1114                    VmValue::Float(n) => VmValue::Float(-n),
1115                    VmValue::Decimal(d) => VmValue::Decimal(-d),
1116                    other => {
1117                        return Err(runtime_err(format!("Cannot negate {}", other.type_name())));
1118                    }
1119                };
1120                self.stack[base + a as usize] = result;
1121            }
1122            Op::Eq => {
1123                let result = self.vm_eq(base, b, c);
1124                self.stack[base + a as usize] = VmValue::Bool(result);
1125            }
1126            Op::Neq => {
1127                let result = !self.vm_eq(base, b, c);
1128                self.stack[base + a as usize] = VmValue::Bool(result);
1129            }
1130            Op::Lt => {
1131                let result = self.vm_cmp(base, b, c)?;
1132                self.stack[base + a as usize] = VmValue::Bool(result == Some(-1));
1133            }
1134            Op::Gt => {
1135                let result = self.vm_cmp(base, b, c)?;
1136                self.stack[base + a as usize] = VmValue::Bool(result == Some(1));
1137            }
1138            Op::Lte => {
1139                let result = self.vm_cmp(base, b, c)?;
1140                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(-1) | Some(0)));
1141            }
1142            Op::Gte => {
1143                let result = self.vm_cmp(base, b, c)?;
1144                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(0) | Some(1)));
1145            }
1146            Op::And => {
1147                let left = self.stack[base + b as usize].is_truthy();
1148                let right = self.stack[base + c as usize].is_truthy();
1149                self.stack[base + a as usize] = VmValue::Bool(left && right);
1150            }
1151            Op::Or => {
1152                let left = self.stack[base + b as usize].is_truthy();
1153                let right = self.stack[base + c as usize].is_truthy();
1154                self.stack[base + a as usize] = VmValue::Bool(left || right);
1155            }
1156            Op::Not => {
1157                let val = !self.stack[base + b as usize].is_truthy();
1158                self.stack[base + a as usize] = VmValue::Bool(val);
1159            }
1160            Op::Concat => {
1161                let left = format!("{}", self.stack[base + b as usize]);
1162                let right = format!("{}", self.stack[base + c as usize]);
1163                self.stack[base + a as usize] =
1164                    VmValue::String(Arc::from(format!("{left}{right}").as_str()));
1165            }
1166            Op::Jump => {
1167                let frame = &mut self.frames[frame_idx];
1168                frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1169            }
1170            Op::JumpIfFalse => {
1171                if !self.stack[base + a as usize].is_truthy() {
1172                    let frame = &mut self.frames[frame_idx];
1173                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1174                }
1175            }
1176            Op::JumpIfTrue => {
1177                if self.stack[base + a as usize].is_truthy() {
1178                    let frame = &mut self.frames[frame_idx];
1179                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1180                }
1181            }
1182            Op::Call => {
1183                // a = func reg, b = args start, c = arg count
1184                let func_val = self.stack[base + a as usize].clone();
1185                self.do_call(func_val, base, a, b, c)?;
1186            }
1187            Op::Return => {
1188                let return_val = self.stack[base + a as usize].clone();
1189                self.frames.pop();
1190                return Ok(Some(return_val));
1191            }
1192            Op::Closure => {
1193                let proto = match &self.frames[frame_idx].prototype.constants[bx as usize] {
1194                    Constant::Prototype(p) => p.clone(),
1195                    _ => return Err(runtime_err("Expected prototype constant")),
1196                };
1197
1198                // Capture upvalues
1199                let mut upvalues = Vec::new();
1200                for def in &proto.upvalue_defs {
1201                    if def.is_local {
1202                        upvalues.push(UpvalueRef::Open {
1203                            stack_index: base + def.index as usize,
1204                        });
1205                    } else {
1206                        let frame = &self.frames[frame_idx];
1207                        upvalues.push(frame.upvalues[def.index as usize].clone());
1208                    }
1209                }
1210
1211                let closure = VmClosure {
1212                    prototype: proto,
1213                    upvalues,
1214                };
1215                self.stack[base + a as usize] = VmValue::Function(Arc::new(closure));
1216            }
1217            Op::NewList => {
1218                // a = dest, b = start reg, c = count
1219                let mut items = Vec::with_capacity(c as usize);
1220                for i in 0..c as usize {
1221                    items.push(self.stack[base + b as usize + i].clone());
1222                }
1223                self.stack[base + a as usize] = VmValue::List(Box::new(items));
1224            }
1225            Op::GetIndex => {
1226                let raw_obj = &self.stack[base + b as usize];
1227                let obj = match raw_obj {
1228                    VmValue::Ref(inner) => inner.as_ref(),
1229                    other => other,
1230                };
1231                let idx = &self.stack[base + c as usize];
1232                let result = match (obj, idx) {
1233                    (VmValue::List(items), VmValue::Int(i)) => {
1234                        let idx = if *i < 0 {
1235                            let adjusted = items.len() as i64 + *i;
1236                            if adjusted < 0 {
1237                                return Err(runtime_err(format!(
1238                                    "Index {} out of bounds for list of length {}",
1239                                    i,
1240                                    items.len()
1241                                )));
1242                            }
1243                            adjusted as usize
1244                        } else {
1245                            *i as usize
1246                        };
1247                        items.get(idx).cloned().ok_or_else(|| {
1248                            runtime_err(format!(
1249                                "Index {} out of bounds for list of length {}",
1250                                i,
1251                                items.len()
1252                            ))
1253                        })?
1254                    }
1255                    (VmValue::Map(pairs), VmValue::String(key)) => pairs
1256                        .iter()
1257                        .find(|(k, _)| k.as_ref() == key.as_ref())
1258                        .map(|(_, v)| v.clone())
1259                        .unwrap_or(VmValue::None),
1260                    _ => {
1261                        return Err(runtime_err(format!(
1262                            "Cannot index {} with {}",
1263                            obj.type_name(),
1264                            idx.type_name()
1265                        )));
1266                    }
1267                };
1268                self.stack[base + a as usize] = result;
1269            }
1270            Op::SetIndex => {
1271                if matches!(&self.stack[base + b as usize], VmValue::Ref(_)) {
1272                    return Err(runtime_err(
1273                        "Cannot mutate a borrowed reference".to_string(),
1274                    ));
1275                }
1276                let val = self.stack[base + a as usize].clone();
1277                let idx_val = self.stack[base + c as usize].clone();
1278                match idx_val {
1279                    VmValue::Int(i) => {
1280                        if let VmValue::List(ref mut items) = self.stack[base + b as usize] {
1281                            let idx = if i < 0 {
1282                                let adjusted = items.len() as i64 + i;
1283                                if adjusted < 0 {
1284                                    return Err(runtime_err(format!(
1285                                        "Index {} out of bounds for list of length {}",
1286                                        i,
1287                                        items.len()
1288                                    )));
1289                                }
1290                                adjusted as usize
1291                            } else {
1292                                i as usize
1293                            };
1294                            if idx < items.len() {
1295                                items[idx] = val;
1296                            } else {
1297                                return Err(runtime_err(format!(
1298                                    "Index {} out of bounds for list of length {}",
1299                                    i,
1300                                    items.len()
1301                                )));
1302                            }
1303                        }
1304                    }
1305                    VmValue::String(key) => {
1306                        if let VmValue::Map(ref mut pairs) = self.stack[base + b as usize] {
1307                            if let Some(entry) =
1308                                pairs.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref())
1309                            {
1310                                entry.1 = val;
1311                            } else {
1312                                pairs.push((key, val));
1313                            }
1314                        }
1315                    }
1316                    _ => {}
1317                }
1318            }
1319            Op::NewMap => {
1320                // a = dest, b = start reg, c = pair count
1321                // The pairs are key, value, key, value in registers b..b+c*2
1322                let mut pairs = Vec::with_capacity(c as usize);
1323                for i in 0..c as usize {
1324                    let key_val = &self.stack[base + b as usize + i * 2];
1325                    let val = self.stack[base + b as usize + i * 2 + 1].clone();
1326                    let key = match key_val {
1327                        VmValue::String(s) => s.clone(),
1328                        other => Arc::from(format!("{other}").as_str()),
1329                    };
1330                    pairs.push((key, val));
1331                }
1332                self.stack[base + a as usize] = VmValue::Map(Box::new(pairs));
1333            }
1334            Op::TablePipe => {
1335                #[cfg(feature = "native")]
1336                {
1337                    // a = table reg, b = op name constant idx, c = args constant idx
1338                    let table_val = self.stack[base + a as usize].clone();
1339                    let result = self.handle_table_pipe(frame_idx, table_val, b, c)?;
1340                    self.stack[base + a as usize] = result;
1341                }
1342                #[cfg(not(feature = "native"))]
1343                {
1344                    let _ = (a, b, c, frame_idx);
1345                    return Err(runtime_err("Table operations not available in WASM"));
1346                }
1347            }
1348            Op::CallBuiltin => {
1349                // ABx format: a = dest, bx = builtin id (16-bit)
1350                // Next instruction: A = arg count, B = first arg reg
1351                let builtin_id = decode_bx(inst);
1352                let next_inst = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1353                self.frames[frame_idx].ip += 1;
1354                let arg_count = decode_a(next_inst) as usize;
1355                let first_arg = decode_b(next_inst) as usize;
1356
1357                let result = self.call_builtin(builtin_id, base + first_arg, arg_count)?;
1358                self.stack[base + a as usize] = result;
1359            }
1360            Op::ForIter => {
1361                // a = iterator (index) reg, b = list reg, c = value dest reg
1362                let idx = match &self.stack[base + a as usize] {
1363                    VmValue::Int(i) => *i as usize,
1364                    _ => 0,
1365                };
1366                let list = &self.stack[base + b as usize];
1367                let done = match list {
1368                    VmValue::List(items) if idx < items.len() => {
1369                        let item = items[idx].clone();
1370                        self.stack[base + c as usize] = item;
1371                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1372                        false
1373                    }
1374                    VmValue::Map(pairs) if idx < pairs.len() => {
1375                        let (k, v) = &pairs[idx];
1376                        let pair =
1377                            VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]));
1378                        self.stack[base + c as usize] = pair;
1379                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1380                        false
1381                    }
1382                    VmValue::Set(items) if idx < items.len() => {
1383                        let item = items[idx].clone();
1384                        self.stack[base + c as usize] = item;
1385                        self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1386                        false
1387                    }
1388                    VmValue::Generator(gen_arc) => {
1389                        let g = gen_arc.clone();
1390                        let val = self.generator_next(&g)?;
1391                        if matches!(val, VmValue::None) {
1392                            true
1393                        } else {
1394                            self.stack[base + c as usize] = val;
1395                            false
1396                        }
1397                    }
1398                    _ => true,
1399                };
1400                if done {
1401                    // Next instruction is a Jump — execute it
1402                    // (the jump instruction follows ForIter)
1403                } else {
1404                    // Skip the jump instruction
1405                    self.frames[frame_idx].ip += 1;
1406                }
1407            }
1408            Op::ForPrep => {
1409                // Not currently used — ForIter handles everything
1410            }
1411            Op::TestMatch => {
1412                // a = subject reg, b = pattern reg, c = dest bool reg
1413                let subject = &self.stack[base + a as usize];
1414                let pattern = &self.stack[base + b as usize];
1415                let matched = match (subject, pattern) {
1416                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
1417                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
1418                    (VmValue::String(a), VmValue::String(b)) => a == b,
1419                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1420                    (VmValue::None, VmValue::None) => true,
1421                    // Enum instance matching: same type + same variant
1422                    (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1423                        subj.type_name == pat.type_name && subj.variant == pat.variant
1424                    }
1425                    // Struct instance matching by type name
1426                    (VmValue::StructInstance(s), VmValue::String(name)) => {
1427                        s.type_name.as_ref() == name.as_ref()
1428                    }
1429                    _ => false,
1430                };
1431                self.stack[base + c as usize] = VmValue::Bool(matched);
1432            }
1433            Op::NullCoalesce => {
1434                if matches!(self.stack[base + a as usize], VmValue::None) {
1435                    let val = self.stack[base + b as usize].clone();
1436                    self.stack[base + a as usize] = val;
1437                }
1438            }
1439            Op::GetMember => {
1440                // a = dest, b = object reg, c = field name constant
1441                let field_name = self.get_string_constant(frame_idx, c as u16)?;
1442                let raw_obj = self.stack[base + b as usize].clone();
1443                let obj = match &raw_obj {
1444                    VmValue::Ref(inner) => inner.as_ref().clone(),
1445                    _ => raw_obj,
1446                };
1447                let result = match &obj {
1448                    VmValue::StructInstance(inst) => inst
1449                        .fields
1450                        .iter()
1451                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1452                        .map(|(_, v)| v.clone())
1453                        .unwrap_or(VmValue::None),
1454                    VmValue::Module(m) => m
1455                        .exports
1456                        .get(field_name.as_ref())
1457                        .cloned()
1458                        .unwrap_or(VmValue::None),
1459                    VmValue::EnumInstance(e) => match field_name.as_ref() {
1460                        "variant" => VmValue::String(e.variant.clone()),
1461                        "type_name" => VmValue::String(e.type_name.clone()),
1462                        _ => VmValue::None,
1463                    },
1464                    VmValue::Map(pairs) => pairs
1465                        .iter()
1466                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1467                        .map(|(_, v)| v.clone())
1468                        .unwrap_or(VmValue::None),
1469                    #[cfg(feature = "python")]
1470                    VmValue::PyObject(wrapper) => {
1471                        crate::python::py_get_member(wrapper, field_name.as_ref())
1472                    }
1473                    _ => VmValue::None,
1474                };
1475                self.stack[base + a as usize] = result;
1476            }
1477            Op::Interpolate => {
1478                // a = dest, bx = string template constant
1479                let template = self.get_string_constant(frame_idx, bx)?;
1480                let result = self.interpolate_string(&template, base)?;
1481                self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1482            }
1483            Op::Train => {
1484                #[cfg(feature = "native")]
1485                {
1486                    let result = self.handle_train(frame_idx, b, c)?;
1487                    self.stack[base + a as usize] = result;
1488                }
1489                #[cfg(not(feature = "native"))]
1490                {
1491                    let _ = (a, b, c, frame_idx);
1492                    return Err(runtime_err("AI training not available in WASM"));
1493                }
1494            }
1495            Op::PipelineExec => {
1496                #[cfg(feature = "native")]
1497                {
1498                    let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1499                    self.stack[base + a as usize] = result;
1500                }
1501                #[cfg(not(feature = "native"))]
1502                {
1503                    let _ = (a, b, c, frame_idx);
1504                    return Err(runtime_err("Pipelines not available in WASM"));
1505                }
1506            }
1507            Op::StreamExec => {
1508                #[cfg(feature = "native")]
1509                {
1510                    let result = self.handle_stream_exec(frame_idx, b)?;
1511                    self.stack[base + a as usize] = result;
1512                }
1513                #[cfg(not(feature = "native"))]
1514                {
1515                    let _ = (a, b, frame_idx);
1516                    return Err(runtime_err("Streaming not available in WASM"));
1517                }
1518            }
1519            Op::ConnectorDecl => {
1520                #[cfg(feature = "native")]
1521                {
1522                    let result = self.handle_connector_decl(frame_idx, b, c)?;
1523                    self.stack[base + a as usize] = result;
1524                }
1525                #[cfg(not(feature = "native"))]
1526                {
1527                    let _ = (a, b, c, frame_idx);
1528                    return Err(runtime_err("Connectors not available in WASM"));
1529                }
1530            }
1531
1532            // ── Phase 5: Language completeness opcodes ──
1533            Op::NewStruct => {
1534                // Two uses:
1535                // 1) Struct declaration: a=dest, b=name_const, c=fields_const (AstExprList)
1536                //    Next instruction is NOT a Move with start reg
1537                // 2) Struct instance: a=dest, b=name_const, c=field_count
1538                //    Next instruction is Move with start reg in A
1539
1540                let name = self.get_string_constant(frame_idx, b as u16)?;
1541
1542                // High bit of c distinguishes declaration (set) from instance (clear).
1543                // Declarations: c = constant_idx | 0x80
1544                // Instances: c = field_count (no high bit)
1545                let is_decl = (c & 0x80) != 0;
1546
1547                if is_decl {
1548                    let const_idx = (c & 0x7F) as usize;
1549                    // Struct/Enum declaration
1550                    let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1551                        Constant::AstExprList(exprs) => exprs.clone(),
1552                        _ => Vec::new(),
1553                    };
1554                    // Check if it looks like an enum (fields have "Name:count" format)
1555                    let is_enum = fields_data
1556                        .first()
1557                        .map(|e| {
1558                            if let AstExpr::String(s) = e {
1559                                s.contains(':')
1560                            } else {
1561                                false
1562                            }
1563                        })
1564                        .unwrap_or(false);
1565
1566                    if is_enum {
1567                        let variants: Vec<(Arc<str>, usize)> = fields_data
1568                            .iter()
1569                            .filter_map(|e| {
1570                                if let AstExpr::String(s) = e {
1571                                    let parts: Vec<&str> = s.splitn(2, ':').collect();
1572                                    if parts.len() == 2 {
1573                                        Some((
1574                                            Arc::from(parts[0]),
1575                                            parts[1].parse::<usize>().unwrap_or(0),
1576                                        ))
1577                                    } else {
1578                                        None
1579                                    }
1580                                } else {
1581                                    None
1582                                }
1583                            })
1584                            .collect();
1585                        self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1586                            name: name.clone(),
1587                            variants,
1588                        }));
1589                    } else {
1590                        let field_names: Vec<Arc<str>> = fields_data
1591                            .iter()
1592                            .filter_map(|e| {
1593                                if let AstExpr::String(s) = e {
1594                                    Some(Arc::from(s.as_str()))
1595                                } else {
1596                                    None
1597                                }
1598                            })
1599                            .collect();
1600                        self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1601                            name: name.clone(),
1602                            fields: field_names,
1603                        }));
1604                    }
1605                } else {
1606                    // Struct instance creation: c = field count
1607                    let field_count = c as usize;
1608                    // Next instruction holds start register in A field
1609                    let next_ip = self.frames[frame_idx].ip;
1610                    let next = self.frames[frame_idx]
1611                        .prototype
1612                        .code
1613                        .get(next_ip)
1614                        .copied()
1615                        .unwrap_or(0);
1616                    let start_reg = decode_a(next) as usize;
1617                    self.frames[frame_idx].ip += 1; // skip the extra instruction
1618
1619                    let mut fields = Vec::new();
1620                    for i in 0..field_count {
1621                        let fname = self.stack[base + start_reg + i * 2].clone();
1622                        let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1623                        let fname_str = match fname {
1624                            VmValue::String(s) => s,
1625                            _ => Arc::from(format!("field_{i}").as_str()),
1626                        };
1627                        fields.push((fname_str, fval));
1628                    }
1629                    self.stack[base + a as usize] =
1630                        VmValue::StructInstance(Arc::new(VmStructInstance {
1631                            type_name: name.clone(),
1632                            fields,
1633                        }));
1634                }
1635            }
1636
1637            Op::SetMember => {
1638                if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1639                    return Err(runtime_err(
1640                        "Cannot mutate a borrowed reference".to_string(),
1641                    ));
1642                }
1643                // a = object reg, b = field name constant, c = value reg
1644                let field_name = self.get_string_constant(frame_idx, b as u16)?;
1645                let val = self.stack[base + c as usize].clone();
1646                let obj = self.stack[base + a as usize].clone();
1647                if let VmValue::StructInstance(inst) = obj {
1648                    let mut new_fields = inst.fields.clone();
1649                    let mut found = false;
1650                    for (k, v) in &mut new_fields {
1651                        if k.as_ref() == field_name.as_ref() {
1652                            *v = val.clone();
1653                            found = true;
1654                            break;
1655                        }
1656                    }
1657                    if !found {
1658                        new_fields.push((field_name, val));
1659                    }
1660                    self.stack[base + a as usize] =
1661                        VmValue::StructInstance(Arc::new(VmStructInstance {
1662                            type_name: inst.type_name.clone(),
1663                            fields: new_fields,
1664                        }));
1665                }
1666            }
1667
1668            Op::NewEnum => {
1669                // a = dest, b = name constant ("EnumName::Variant"), c = args start reg
1670                // Next instruction: arg_count in A field
1671                let full_name = self.get_string_constant(frame_idx, b as u16)?;
1672                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1673                self.frames[frame_idx].ip += 1;
1674                let arg_count = decode_a(next) as usize;
1675                let args_start = c as usize;
1676
1677                // Parse "EnumName::Variant"
1678                let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1679                let (type_name, variant) = if parts.len() == 2 {
1680                    (Arc::from(parts[0]), Arc::from(parts[1]))
1681                } else {
1682                    (Arc::from(""), Arc::from(full_name.as_ref()))
1683                };
1684
1685                let mut fields = Vec::new();
1686                for i in 0..arg_count {
1687                    fields.push(self.stack[base + args_start + i].clone());
1688                }
1689
1690                self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1691                    type_name,
1692                    variant,
1693                    fields,
1694                }));
1695            }
1696
1697            Op::MatchEnum => {
1698                // a = subject reg, b = variant name constant, c = dest bool reg
1699                let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1700                let subject = &self.stack[base + a as usize];
1701                let matched = match subject {
1702                    VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1703                    _ => false,
1704                };
1705                self.stack[base + c as usize] = VmValue::Bool(matched);
1706            }
1707
1708            Op::MethodCall => {
1709                // a = dest, b = object reg, c = method name constant
1710                // Next instruction: A = args_start, B = arg_count
1711                let method_name = self.get_string_constant(frame_idx, c as u16)?;
1712                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1713                self.frames[frame_idx].ip += 1;
1714                let args_start = decode_a(next) as usize;
1715                let arg_count = decode_b(next) as usize;
1716
1717                let obj = self.stack[base + b as usize].clone();
1718                let mut args = Vec::new();
1719                for i in 0..arg_count {
1720                    args.push(self.stack[base + args_start + i].clone());
1721                }
1722
1723                let result = self.dispatch_method(obj, &method_name, &args)?;
1724                self.stack[base + a as usize] = result;
1725            }
1726
1727            Op::Throw => {
1728                // a = value register
1729                let val = self.stack[base + a as usize].clone();
1730                self.thrown_value = Some(val.clone());
1731                let err_msg = format!("{val}");
1732                return Err(runtime_err(err_msg));
1733            }
1734
1735            Op::TryBegin => {
1736                // sbx = offset to catch handler (relative to this instruction)
1737                let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1738                self.try_handlers.push(TryHandler {
1739                    frame_idx: self.frames.len(),
1740                    catch_ip,
1741                });
1742            }
1743
1744            Op::TryEnd => {
1745                // Pop the try handler (success path)
1746                self.try_handlers.pop();
1747            }
1748
1749            Op::Import => {
1750                #[cfg(feature = "native")]
1751                {
1752                    // a = dest, bx = path constant
1753                    // Next instruction encodes either:
1754                    //   - Classic import: A = alias constant, B = 0, C = 0
1755                    //   - Use import: A = extra, B = kind, C = 0xAB (magic marker)
1756                    let path = self.get_string_constant(frame_idx, bx)?;
1757                    let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1758                    self.frames[frame_idx].ip += 1;
1759                    let next_a = decode_a(next);
1760                    let next_b = decode_b(next);
1761                    let next_c = decode_c(next);
1762
1763                    let result = if next_c == 0xAB {
1764                        // Use-style import (dot-path)
1765                        self.handle_use_import(&path, next_a, next_b, frame_idx)?
1766                    } else {
1767                        // Classic import "file.tl" [as alias]
1768                        let alias_idx = next_a as u16;
1769                        let alias = self.get_string_constant(frame_idx, alias_idx)?;
1770                        self.handle_import(&path, &alias)?
1771                    };
1772                    self.stack[base + a as usize] = result;
1773                }
1774                #[cfg(not(feature = "native"))]
1775                {
1776                    let _ = (a, bx, frame_idx);
1777                    return Err(runtime_err("Module imports not available in WASM"));
1778                }
1779            }
1780
1781            Op::Await => {
1782                // a = dest, b = task/value register
1783                let val = self.stack[base + b as usize].clone();
1784                match val {
1785                    VmValue::Task(task) => {
1786                        let rx = {
1787                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1788                            guard.take()
1789                        };
1790                        match rx {
1791                            Some(receiver) => match receiver.recv() {
1792                                Ok(Ok(result)) => {
1793                                    self.stack[base + a as usize] = result;
1794                                }
1795                                Ok(Err(err_msg)) => {
1796                                    return Err(runtime_err(err_msg));
1797                                }
1798                                Err(_) => {
1799                                    return Err(runtime_err("Task channel disconnected"));
1800                                }
1801                            },
1802                            None => {
1803                                return Err(runtime_err("Task already awaited"));
1804                            }
1805                        }
1806                    }
1807                    // Non-task values pass through
1808                    other => {
1809                        self.stack[base + a as usize] = other;
1810                    }
1811                }
1812            }
1813            Op::Yield => {
1814                // a = value register to yield
1815                let val = self.stack[base + a as usize].clone();
1816                self.yielded_value = Some(val.clone());
1817                // Save the current ip (already advanced past Yield instruction)
1818                self.yielded_ip = self.frames[frame_idx].ip;
1819                // Pop the frame and return the value
1820                self.frames.pop();
1821                return Ok(Some(val));
1822            }
1823            Op::TryPropagate => {
1824                // A = dest, B = source register
1825                // If source is Err(...) → early return from current function
1826                // If source is Ok(v) → A = v (unwrap)
1827                // If source is None → early return None
1828                // Otherwise → passthrough
1829                let src = self.stack[base + b as usize].clone();
1830                match &src {
1831                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1832                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1833                            // Unwrap: A = inner value
1834                            self.stack[base + a as usize] = ei.fields[0].clone();
1835                        } else if ei.variant.as_ref() == "Err" {
1836                            // Propagate: return the Err from current function
1837                            self.frames.pop();
1838                            return Ok(Some(src));
1839                        } else {
1840                            self.stack[base + a as usize] = src;
1841                        }
1842                    }
1843                    VmValue::None => {
1844                        // Propagate: return None from current function
1845                        self.frames.pop();
1846                        return Ok(Some(VmValue::None));
1847                    }
1848                    _ => {
1849                        // Passthrough
1850                        self.stack[base + a as usize] = src;
1851                    }
1852                }
1853            }
1854            Op::ExtractField => {
1855                // A = dest, B = source reg, C = field index
1856                // If C has high bit set (C | 0x80), extract rest (sublist from index C & 0x7F)
1857                let source = self.stack[base + b as usize].clone();
1858                let is_rest = (c & 0x80) != 0;
1859                let idx = (c & 0x7F) as usize;
1860                let val = if is_rest {
1861                    // Extract rest as sublist from index idx..
1862                    match &source {
1863                        VmValue::List(l) => {
1864                            if idx < l.len() {
1865                                VmValue::List(Box::new(l[idx..].to_vec()))
1866                            } else {
1867                                VmValue::List(Box::default())
1868                            }
1869                        }
1870                        _ => VmValue::List(Box::default()),
1871                    }
1872                } else {
1873                    match &source {
1874                        VmValue::EnumInstance(ei) => {
1875                            ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1876                        }
1877                        VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1878                        _ => VmValue::None,
1879                    }
1880                };
1881                self.stack[base + a as usize] = val;
1882            }
1883            Op::ExtractNamedField => {
1884                // A = dest, B = source reg, C = field name constant index
1885                let source = self.stack[base + b as usize].clone();
1886                let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1887                    Constant::String(s) => s.clone(),
1888                    _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1889                };
1890                let val = match &source {
1891                    VmValue::StructInstance(s) => s
1892                        .fields
1893                        .iter()
1894                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1895                        .map(|(_, v)| v.clone())
1896                        .unwrap_or(VmValue::None),
1897                    VmValue::Map(m) => m
1898                        .iter()
1899                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1900                        .map(|(_, v)| v.clone())
1901                        .unwrap_or(VmValue::None),
1902                    _ => VmValue::None,
1903                };
1904                self.stack[base + a as usize] = val;
1905            }
1906
1907            // Phase 28: Ownership & Move Semantics
1908            Op::LoadMoved => {
1909                self.stack[base + a as usize] = VmValue::Moved;
1910            }
1911            Op::MakeRef => {
1912                let val = self.stack[base + b as usize].clone();
1913                self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1914            }
1915            Op::ParallelFor => {
1916                // Currently compiled as regular ForIter, this opcode is reserved
1917                // for future rayon-backed parallel iteration.
1918            }
1919            Op::AgentExec => {
1920                #[cfg(feature = "native")]
1921                {
1922                    let result = self.handle_agent_exec(frame_idx, b, c)?;
1923                    self.stack[base + a as usize] = result;
1924                }
1925                #[cfg(not(feature = "native"))]
1926                {
1927                    let _ = (a, b, c, frame_idx);
1928                    return Err(runtime_err("Agents not available in WASM".to_string()));
1929                }
1930            }
1931        }
1932        Ok(None)
1933    }
1934
1935    /// Perform a function call.
1936    fn do_call(
1937        &mut self,
1938        func: VmValue,
1939        caller_base: usize,
1940        func_reg: u8,
1941        args_start: u8,
1942        arg_count: u8,
1943    ) -> Result<(), TlError> {
1944        const MAX_CALL_DEPTH: usize = 512;
1945        if self.frames.len() >= MAX_CALL_DEPTH {
1946            return Err(runtime_err(
1947                "Stack overflow: maximum recursion depth (512) exceeded",
1948            ));
1949        }
1950        match func {
1951            VmValue::Function(closure) => {
1952                let proto = closure.prototype.clone();
1953                let arity = proto.arity as usize;
1954
1955                if arg_count as usize != arity {
1956                    return Err(runtime_err(format!(
1957                        "Expected {} arguments, got {}",
1958                        arity, arg_count
1959                    )));
1960                }
1961
1962                // If this is a generator function, create a Generator instead of executing
1963                if proto.is_generator {
1964                    // Close upvalues for the generator
1965                    let mut closed_upvalues = Vec::new();
1966                    for uv in &closure.upvalues {
1967                        match uv {
1968                            UpvalueRef::Open { stack_index } => {
1969                                let val = self.stack[*stack_index].clone();
1970                                closed_upvalues.push(UpvalueRef::Closed(val));
1971                            }
1972                            UpvalueRef::Closed(v) => {
1973                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1974                            }
1975                        }
1976                    }
1977
1978                    // Build initial saved_stack with args
1979                    let num_regs = proto.num_registers as usize;
1980                    let mut saved_stack = vec![VmValue::None; num_regs];
1981                    for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1982                        *slot = self.stack[caller_base + args_start as usize + i].clone();
1983                    }
1984
1985                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
1986                        prototype: proto,
1987                        upvalues: closed_upvalues,
1988                        saved_stack,
1989                        ip: 0,
1990                    });
1991                    self.stack[caller_base + func_reg as usize] =
1992                        VmValue::Generator(Arc::new(Mutex::new(gn)));
1993                    return Ok(());
1994                }
1995
1996                // Set up new frame
1997                let new_base = self.stack.len();
1998                self.ensure_stack(new_base + proto.num_registers as usize + 1);
1999
2000                // Copy args to new frame's registers
2001                for i in 0..arg_count as usize {
2002                    self.stack[new_base + i] =
2003                        self.stack[caller_base + args_start as usize + i].clone();
2004                }
2005
2006                self.frames.push(CallFrame {
2007                    prototype: proto,
2008                    ip: 0,
2009                    base: new_base,
2010                    upvalues: closure.upvalues.clone(),
2011                });
2012
2013                // Run the function
2014                let result = self.run()?;
2015
2016                // Close any upvalues in the result that point into this frame's stack
2017                let result = self.close_upvalues_in_value(result, new_base);
2018
2019                // Store result in caller's func_reg
2020                self.stack[caller_base + func_reg as usize] = result;
2021
2022                // Shrink stack back
2023                self.stack.truncate(new_base);
2024
2025                Ok(())
2026            }
2027            VmValue::Builtin(builtin_id) => {
2028                let result = self.call_builtin(
2029                    builtin_id as u16,
2030                    caller_base + args_start as usize,
2031                    arg_count as usize,
2032                )?;
2033                self.stack[caller_base + func_reg as usize] = result;
2034                Ok(())
2035            }
2036            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2037        }
2038    }
2039
2040    /// Walk a VmValue and promote any Open upvalues pointing at or above `frame_base`
2041    /// to Closed. This is called on return values before the caller's stack is truncated,
2042    /// so that closures escaping their defining function retain correct captured values.
2043    /// Check if a value may contain functions with open upvalues (recursive).
2044    fn value_may_need_closing(val: &VmValue) -> bool {
2045        match val {
2046            VmValue::Function(_) => true,
2047            VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2048            VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2049            _ => false,
2050        }
2051    }
2052
2053    fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2054        match val {
2055            VmValue::Function(ref closure) => {
2056                // Check if any upvalue needs closing
2057                let needs_closing = closure.upvalues.iter().any(|uv| {
2058                    matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2059                });
2060                if !needs_closing {
2061                    return val;
2062                }
2063                let closed_upvalues: Vec<UpvalueRef> = closure
2064                    .upvalues
2065                    .iter()
2066                    .map(|uv| match uv {
2067                        UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2068                            UpvalueRef::Closed(self.stack[*stack_index].clone())
2069                        }
2070                        other => other.clone(),
2071                    })
2072                    .collect();
2073                VmValue::Function(Arc::new(VmClosure {
2074                    prototype: closure.prototype.clone(),
2075                    upvalues: closed_upvalues,
2076                }))
2077            }
2078            VmValue::List(items) => {
2079                if !items.iter().any(Self::value_may_need_closing) {
2080                    return VmValue::List(items);
2081                }
2082                VmValue::List(Box::new(
2083                    (*items)
2084                        .into_iter()
2085                        .map(|v| self.close_upvalues_in_value(v, frame_base))
2086                        .collect(),
2087                ))
2088            }
2089            VmValue::Map(entries) => {
2090                if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2091                    return VmValue::Map(entries);
2092                }
2093                VmValue::Map(Box::new(
2094                    (*entries)
2095                        .into_iter()
2096                        .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2097                        .collect(),
2098                ))
2099            }
2100            other => other,
2101        }
2102    }
2103
2104    /// Execute a closure (no arguments) in this VM. Used by spawn().
2105    pub(crate) fn execute_closure(
2106        &mut self,
2107        proto: &Arc<Prototype>,
2108        upvalues: &[UpvalueRef],
2109    ) -> Result<VmValue, TlError> {
2110        let base = self.stack.len();
2111        self.ensure_stack(base + proto.num_registers as usize + 1);
2112        self.frames.push(CallFrame {
2113            prototype: proto.clone(),
2114            ip: 0,
2115            base,
2116            upvalues: upvalues.to_vec(),
2117        });
2118        self.run()
2119    }
2120
2121    /// Execute a closure with arguments in this VM. Used by pmap().
2122    pub(crate) fn execute_closure_with_args(
2123        &mut self,
2124        proto: &Arc<Prototype>,
2125        upvalues: &[UpvalueRef],
2126        args: &[VmValue],
2127    ) -> Result<VmValue, TlError> {
2128        let base = self.stack.len();
2129        self.ensure_stack(base + proto.num_registers as usize + 1);
2130        for (i, arg) in args.iter().enumerate() {
2131            self.stack[base + i] = arg.clone();
2132        }
2133        self.frames.push(CallFrame {
2134            prototype: proto.clone(),
2135            ip: 0,
2136            base,
2137            upvalues: upvalues.to_vec(),
2138        });
2139        self.run()
2140    }
2141
2142    fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2143        let frame = &self.frames[frame_idx];
2144        match &frame.prototype.constants[idx as usize] {
2145            Constant::Int(n) => Ok(VmValue::Int(*n)),
2146            Constant::Float(f) => Ok(VmValue::Float(*f)),
2147            Constant::String(s) => Ok(VmValue::String(s.clone())),
2148            Constant::Prototype(p) => {
2149                // Return as a closure with no upvalues
2150                Ok(VmValue::Function(Arc::new(VmClosure {
2151                    prototype: p.clone(),
2152                    upvalues: Vec::new(),
2153                })))
2154            }
2155            Constant::Decimal(s) => {
2156                use std::str::FromStr;
2157                Ok(VmValue::Decimal(
2158                    rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2159                ))
2160            }
2161            Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2162        }
2163    }
2164
2165    fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2166        let frame = &self.frames[frame_idx];
2167        match &frame.prototype.constants[idx as usize] {
2168            Constant::String(s) => Ok(s.clone()),
2169            _ => Err(runtime_err("Expected string constant")),
2170        }
2171    }
2172
2173    // ── Arithmetic helpers ──
2174
2175    fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2176        let left = &self.stack[base + b as usize];
2177        let right = &self.stack[base + c as usize];
2178        match (left, right) {
2179            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2180                .checked_add(*b)
2181                .map(VmValue::Int)
2182                .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2183            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2184            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2185            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2186            (VmValue::String(a), VmValue::String(b)) => {
2187                Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2188            }
2189            #[cfg(feature = "gpu")]
2190            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2191                let a = a.clone();
2192                let b = b.clone();
2193                let ops = self.get_gpu_ops()?;
2194                let result = ops.add(&a, &b).map_err(runtime_err)?;
2195                Ok(VmValue::GpuTensor(Arc::new(result)))
2196            }
2197            #[cfg(feature = "gpu")]
2198            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2199            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2200                let lv = self.stack[base + b as usize].clone();
2201                let rv = self.stack[base + c as usize].clone();
2202                let a = self.ensure_gpu_tensor(&lv)?;
2203                let b_val = self.ensure_gpu_tensor(&rv)?;
2204                let ops = self.get_gpu_ops()?;
2205                let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2206                Ok(VmValue::GpuTensor(Arc::new(result)))
2207            }
2208            #[cfg(feature = "native")]
2209            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2210                let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2211                Ok(VmValue::Tensor(Arc::new(result)))
2212            }
2213            // Decimal arithmetic
2214            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2215            (VmValue::Decimal(a), VmValue::Int(b)) => {
2216                Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2217            }
2218            (VmValue::Int(a), VmValue::Decimal(b)) => {
2219                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2220            }
2221            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2222            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2223            _ => Err(runtime_err(format!(
2224                "Cannot apply `+` to {} and {}",
2225                left.type_name(),
2226                right.type_name()
2227            ))),
2228        }
2229    }
2230
2231    fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2232        let left = &self.stack[base + b as usize];
2233        let right = &self.stack[base + c as usize];
2234        match (left, right) {
2235            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2236                .checked_sub(*b)
2237                .map(VmValue::Int)
2238                .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2239            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2240            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2241            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2242            #[cfg(feature = "gpu")]
2243            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2244                let a = a.clone();
2245                let b = b.clone();
2246                let ops = self.get_gpu_ops()?;
2247                let result = ops.sub(&a, &b).map_err(runtime_err)?;
2248                Ok(VmValue::GpuTensor(Arc::new(result)))
2249            }
2250            #[cfg(feature = "gpu")]
2251            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2252            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2253                let lv = self.stack[base + b as usize].clone();
2254                let rv = self.stack[base + c as usize].clone();
2255                let a = self.ensure_gpu_tensor(&lv)?;
2256                let b_val = self.ensure_gpu_tensor(&rv)?;
2257                let ops = self.get_gpu_ops()?;
2258                let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2259                Ok(VmValue::GpuTensor(Arc::new(result)))
2260            }
2261            #[cfg(feature = "native")]
2262            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2263                let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2264                Ok(VmValue::Tensor(Arc::new(result)))
2265            }
2266            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2267            (VmValue::Decimal(a), VmValue::Int(b)) => {
2268                Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2269            }
2270            (VmValue::Int(a), VmValue::Decimal(b)) => {
2271                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2272            }
2273            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2274            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2275            _ => Err(runtime_err(format!(
2276                "Cannot apply `-` to {} and {}",
2277                left.type_name(),
2278                right.type_name()
2279            ))),
2280        }
2281    }
2282
2283    fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2284        let left = &self.stack[base + b as usize];
2285        let right = &self.stack[base + c as usize];
2286        match (left, right) {
2287            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2288                .checked_mul(*b)
2289                .map(VmValue::Int)
2290                .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2291            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2292            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2293            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2294            (VmValue::String(a), VmValue::Int(b)) => {
2295                if *b < 0 {
2296                    return Err(runtime_err(
2297                        "Cannot repeat string a negative number of times",
2298                    ));
2299                }
2300                if *b > 10_000_000 {
2301                    return Err(runtime_err(
2302                        "String repeat count too large (max 10,000,000)",
2303                    ));
2304                }
2305                Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2306            }
2307            #[cfg(feature = "gpu")]
2308            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2309                let a = a.clone();
2310                let b = b.clone();
2311                let ops = self.get_gpu_ops()?;
2312                let result = ops.mul(&a, &b).map_err(runtime_err)?;
2313                Ok(VmValue::GpuTensor(Arc::new(result)))
2314            }
2315            #[cfg(feature = "gpu")]
2316            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2317            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2318                let lv = self.stack[base + b as usize].clone();
2319                let rv = self.stack[base + c as usize].clone();
2320                let a = self.ensure_gpu_tensor(&lv)?;
2321                let b_val = self.ensure_gpu_tensor(&rv)?;
2322                let ops = self.get_gpu_ops()?;
2323                let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2324                Ok(VmValue::GpuTensor(Arc::new(result)))
2325            }
2326            #[cfg(feature = "gpu")]
2327            (VmValue::GpuTensor(t), VmValue::Float(s))
2328            | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2329                let t = t.clone();
2330                let s = *s;
2331                let ops = self.get_gpu_ops()?;
2332                let result = ops.scale(&t, s as f32);
2333                Ok(VmValue::GpuTensor(Arc::new(result)))
2334            }
2335            #[cfg(feature = "native")]
2336            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2337                let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2338                Ok(VmValue::Tensor(Arc::new(result)))
2339            }
2340            #[cfg(feature = "native")]
2341            (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2342                let result = t.scale(*s);
2343                Ok(VmValue::Tensor(Arc::new(result)))
2344            }
2345            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2346            (VmValue::Decimal(a), VmValue::Int(b)) => {
2347                Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2348            }
2349            (VmValue::Int(a), VmValue::Decimal(b)) => {
2350                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2351            }
2352            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2353            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2354            _ => Err(runtime_err(format!(
2355                "Cannot apply `*` to {} and {}",
2356                left.type_name(),
2357                right.type_name()
2358            ))),
2359        }
2360    }
2361
2362    fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2363        let left = &self.stack[base + b as usize];
2364        let right = &self.stack[base + c as usize];
2365        match (left, right) {
2366            (VmValue::Int(a), VmValue::Int(b)) => {
2367                if *b == 0 {
2368                    return Err(runtime_err("Division by zero"));
2369                }
2370                Ok(VmValue::Int(a / b))
2371            }
2372            (VmValue::Float(a), VmValue::Float(b)) => {
2373                if *b == 0.0 {
2374                    return Err(runtime_err("Division by zero"));
2375                }
2376                Ok(VmValue::Float(a / b))
2377            }
2378            (VmValue::Int(a), VmValue::Float(b)) => {
2379                if *b == 0.0 {
2380                    return Err(runtime_err("Division by zero"));
2381                }
2382                Ok(VmValue::Float(*a as f64 / b))
2383            }
2384            (VmValue::Float(a), VmValue::Int(b)) => {
2385                if *b == 0 {
2386                    return Err(runtime_err("Division by zero"));
2387                }
2388                Ok(VmValue::Float(a / *b as f64))
2389            }
2390            #[cfg(feature = "gpu")]
2391            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2392                let a = a.clone();
2393                let b = b.clone();
2394                let ops = self.get_gpu_ops()?;
2395                let result = ops.div(&a, &b).map_err(runtime_err)?;
2396                Ok(VmValue::GpuTensor(Arc::new(result)))
2397            }
2398            #[cfg(feature = "gpu")]
2399            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2400            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2401                let lv = self.stack[base + b as usize].clone();
2402                let rv = self.stack[base + c as usize].clone();
2403                let a = self.ensure_gpu_tensor(&lv)?;
2404                let b_val = self.ensure_gpu_tensor(&rv)?;
2405                let ops = self.get_gpu_ops()?;
2406                let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2407                Ok(VmValue::GpuTensor(Arc::new(result)))
2408            }
2409            #[cfg(feature = "native")]
2410            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2411                let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2412                Ok(VmValue::Tensor(Arc::new(result)))
2413            }
2414            (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2415                if b.is_zero() {
2416                    return Err(runtime_err("Division by zero"));
2417                }
2418                Ok(VmValue::Decimal(a / b))
2419            }
2420            (VmValue::Decimal(a), VmValue::Int(b)) => {
2421                if *b == 0 {
2422                    return Err(runtime_err("Division by zero"));
2423                }
2424                Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2425            }
2426            (VmValue::Int(a), VmValue::Decimal(b)) => {
2427                if b.is_zero() {
2428                    return Err(runtime_err("Division by zero"));
2429                }
2430                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2431            }
2432            (VmValue::Decimal(a), VmValue::Float(b)) => {
2433                if *b == 0.0 {
2434                    return Err(runtime_err("Division by zero"));
2435                }
2436                Ok(VmValue::Float(decimal_to_f64(a) / b))
2437            }
2438            (VmValue::Float(a), VmValue::Decimal(b)) => {
2439                if b.is_zero() {
2440                    return Err(runtime_err("Division by zero"));
2441                }
2442                Ok(VmValue::Float(a / decimal_to_f64(b)))
2443            }
2444            _ => Err(runtime_err(format!(
2445                "Cannot apply `/` to {} and {}",
2446                left.type_name(),
2447                right.type_name()
2448            ))),
2449        }
2450    }
2451
2452    fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2453        let left = &self.stack[base + b as usize];
2454        let right = &self.stack[base + c as usize];
2455        match (left, right) {
2456            (VmValue::Int(a), VmValue::Int(b)) => {
2457                if *b == 0 {
2458                    return Err(runtime_err("Modulo by zero"));
2459                }
2460                Ok(VmValue::Int(a % b))
2461            }
2462            (VmValue::Float(a), VmValue::Float(b)) => {
2463                if *b == 0.0 {
2464                    return Err(runtime_err("Modulo by zero"));
2465                }
2466                Ok(VmValue::Float(a % b))
2467            }
2468            (VmValue::Int(a), VmValue::Float(b)) => {
2469                if *b == 0.0 {
2470                    return Err(runtime_err("Modulo by zero"));
2471                }
2472                Ok(VmValue::Float(*a as f64 % b))
2473            }
2474            (VmValue::Float(a), VmValue::Int(b)) => {
2475                if *b == 0 {
2476                    return Err(runtime_err("Modulo by zero"));
2477                }
2478                Ok(VmValue::Float(a % *b as f64))
2479            }
2480            _ => Err(runtime_err(format!(
2481                "Cannot apply `%` to {} and {}",
2482                left.type_name(),
2483                right.type_name()
2484            ))),
2485        }
2486    }
2487
2488    fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2489        let left = &self.stack[base + b as usize];
2490        let right = &self.stack[base + c as usize];
2491        match (left, right) {
2492            (VmValue::Int(a), VmValue::Int(b)) => {
2493                if *b < 0 {
2494                    return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2495                }
2496                match a.checked_pow(*b as u32) {
2497                    Some(result) => Ok(VmValue::Int(result)),
2498                    None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2499                }
2500            }
2501            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2502            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2503            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2504            _ => Err(runtime_err(format!(
2505                "Cannot apply `**` to {} and {}",
2506                left.type_name(),
2507                right.type_name()
2508            ))),
2509        }
2510    }
2511
2512    fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2513        self.stack[base + b as usize] == self.stack[base + c as usize]
2514    }
2515
2516    fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2517        let left = &self.stack[base + b as usize];
2518        let right = &self.stack[base + c as usize];
2519        match (left, right) {
2520            (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2521            (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2522            (VmValue::Int(a), VmValue::Float(b)) => {
2523                let fa = *a as f64;
2524                Ok(fa.partial_cmp(b).map(|o| o as i8))
2525            }
2526            (VmValue::Float(a), VmValue::Int(b)) => {
2527                let fb = *b as f64;
2528                Ok(a.partial_cmp(&fb).map(|o| o as i8))
2529            }
2530            (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2531            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2532            (VmValue::Decimal(a), VmValue::Int(b)) => {
2533                Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2534            }
2535            (VmValue::Int(a), VmValue::Decimal(b)) => {
2536                Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2537            }
2538            (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2539            (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2540            (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2541            _ => Err(runtime_err(format!(
2542                "Cannot compare {} and {}",
2543                left.type_name(),
2544                right.type_name()
2545            ))),
2546        }
2547    }
2548
2549    // ── Security helpers ──
2550
2551    fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2552        if let Some(ref policy) = self.security_policy
2553            && !policy.check(perm)
2554        {
2555            return Err(runtime_err(format!("{perm} blocked by security policy")));
2556        }
2557        Ok(())
2558    }
2559
2560    // ── Builtin dispatch ──
2561
2562    pub fn call_builtin(
2563        &mut self,
2564        id: u16,
2565        args_base: usize,
2566        arg_count: usize,
2567    ) -> Result<VmValue, TlError> {
2568        let args: Vec<VmValue> = (0..arg_count)
2569            .map(|i| {
2570                let val = &self.stack[args_base + i];
2571                // Unwrap Ref transparently for builtin calls
2572                match val {
2573                    VmValue::Ref(inner) => inner.as_ref().clone(),
2574                    other => other.clone(),
2575                }
2576            })
2577            .collect();
2578
2579        let builtin_id: BuiltinId =
2580            BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2581
2582        match builtin_id {
2583            BuiltinId::Print | BuiltinId::Println => {
2584                let mut parts = Vec::new();
2585                for a in &args {
2586                    #[cfg(feature = "native")]
2587                    match a {
2588                        VmValue::Table(t) => {
2589                            let batches =
2590                                self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2591                            let formatted =
2592                                DataEngine::format_batches(&batches).map_err(runtime_err)?;
2593                            parts.push(formatted);
2594                        }
2595                        _ => parts.push(format!("{a}")),
2596                    }
2597                    #[cfg(not(feature = "native"))]
2598                    parts.push(format!("{a}"));
2599                }
2600                let line = parts.join(" ");
2601                println!("{line}");
2602                self.output.push(line);
2603                Ok(VmValue::None)
2604            }
2605            BuiltinId::Len => match args.first() {
2606                Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2607                Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2608                Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2609                Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2610                _ => Err(runtime_err("len() expects a string, list, map, or set")),
2611            },
2612            BuiltinId::Str => Ok(VmValue::String(Arc::from(
2613                args.first()
2614                    .map(|v| format!("{v}"))
2615                    .unwrap_or_default()
2616                    .as_str(),
2617            ))),
2618            BuiltinId::Int => match args.first() {
2619                Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2620                Some(VmValue::String(s)) => s
2621                    .parse::<i64>()
2622                    .map(VmValue::Int)
2623                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2624                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2625                Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2626                _ => Err(runtime_err("int() expects a number, string, or bool")),
2627            },
2628            BuiltinId::Float => match args.first() {
2629                Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2630                Some(VmValue::String(s)) => s
2631                    .parse::<f64>()
2632                    .map(VmValue::Float)
2633                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2634                Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2635                Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2636                _ => Err(runtime_err("float() expects a number, string, or bool")),
2637            },
2638            BuiltinId::Abs => match args.first() {
2639                Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2640                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2641                _ => Err(runtime_err("abs() expects a number")),
2642            },
2643            BuiltinId::Min => {
2644                if args.len() == 2 {
2645                    match (&args[0], &args[1]) {
2646                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2647                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2648                        _ => Err(runtime_err("min() expects two numbers")),
2649                    }
2650                } else {
2651                    Err(runtime_err("min() expects 2 arguments"))
2652                }
2653            }
2654            BuiltinId::Max => {
2655                if args.len() == 2 {
2656                    match (&args[0], &args[1]) {
2657                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2658                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2659                        _ => Err(runtime_err("max() expects two numbers")),
2660                    }
2661                } else {
2662                    Err(runtime_err("max() expects 2 arguments"))
2663                }
2664            }
2665            BuiltinId::Range => {
2666                if args.len() == 1 {
2667                    if let VmValue::Int(n) = &args[0] {
2668                        if *n > 10_000_000 {
2669                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2670                        }
2671                        if *n < 0 {
2672                            return Ok(VmValue::List(Box::default()));
2673                        }
2674                        Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2675                    } else {
2676                        Err(runtime_err("range() expects an integer"))
2677                    }
2678                } else if args.len() == 2 {
2679                    if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2680                        let size = (*end - *start).max(0);
2681                        if size > 10_000_000 {
2682                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2683                        }
2684                        Ok(VmValue::List(Box::new(
2685                            (*start..*end).map(VmValue::Int).collect(),
2686                        )))
2687                    } else {
2688                        Err(runtime_err("range() expects integers"))
2689                    }
2690                } else if args.len() == 3 {
2691                    if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2692                        (&args[0], &args[1], &args[2])
2693                    {
2694                        if *step == 0 {
2695                            return Err(runtime_err("range() step cannot be zero"));
2696                        }
2697                        let mut result = Vec::new();
2698                        let mut i = *start;
2699                        if *step > 0 {
2700                            while i < *end {
2701                                result.push(VmValue::Int(i));
2702                                i += step;
2703                            }
2704                        } else {
2705                            while i > *end {
2706                                result.push(VmValue::Int(i));
2707                                i += step;
2708                            }
2709                        }
2710                        Ok(VmValue::List(Box::new(result)))
2711                    } else {
2712                        Err(runtime_err("range() expects integers"))
2713                    }
2714                } else {
2715                    Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2716                }
2717            }
2718            BuiltinId::Push => {
2719                if args.len() == 2 {
2720                    if let VmValue::List(mut items) = args[0].clone() {
2721                        items.push(args[1].clone());
2722                        Ok(VmValue::List(items))
2723                    } else {
2724                        Err(runtime_err("push() first arg must be a list"))
2725                    }
2726                } else {
2727                    Err(runtime_err("push() expects 2 arguments"))
2728                }
2729            }
2730            BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2731                args.first().map(|v| v.type_name()).unwrap_or("none"),
2732            ))),
2733            BuiltinId::Map => {
2734                if args.len() != 2 {
2735                    return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2736                }
2737                let items = match &args[0] {
2738                    VmValue::List(items) => (**items).clone(),
2739                    _ => return Err(runtime_err("map() first arg must be a list")),
2740                };
2741                let func = args[1].clone();
2742                // Parallel path for large lists with pure functions
2743                #[cfg(feature = "native")]
2744                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2745                    let proto = match &func {
2746                        VmValue::Function(c) => c.prototype.clone(),
2747                        _ => unreachable!(),
2748                    };
2749                    let result: Result<Vec<VmValue>, TlError> = items
2750                        .into_par_iter()
2751                        .map(|item| execute_pure_fn(&proto, &[item]))
2752                        .collect();
2753                    return Ok(VmValue::List(Box::new(result?)));
2754                }
2755                let mut result = Vec::new();
2756                for item in items {
2757                    let val = self.call_vm_function(&func, &[item])?;
2758                    result.push(val);
2759                }
2760                Ok(VmValue::List(Box::new(result)))
2761            }
2762            BuiltinId::Filter => {
2763                if args.len() != 2 {
2764                    return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2765                }
2766                let items = match &args[0] {
2767                    VmValue::List(items) => (**items).clone(),
2768                    _ => return Err(runtime_err("filter() first arg must be a list")),
2769                };
2770                let func = args[1].clone();
2771                // Parallel path for large lists with pure functions
2772                #[cfg(feature = "native")]
2773                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2774                    let proto = match &func {
2775                        VmValue::Function(c) => c.prototype.clone(),
2776                        _ => unreachable!(),
2777                    };
2778                    let result: Result<Vec<VmValue>, TlError> = items
2779                        .into_par_iter()
2780                        .filter_map(|item| {
2781                            match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2782                                Ok(val) => {
2783                                    if val.is_truthy() {
2784                                        Some(Ok(item))
2785                                    } else {
2786                                        None
2787                                    }
2788                                }
2789                                Err(e) => Some(Err(e)),
2790                            }
2791                        })
2792                        .collect();
2793                    return Ok(VmValue::List(Box::new(result?)));
2794                }
2795                let mut result = Vec::new();
2796                for item in items {
2797                    let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2798                    if val.is_truthy() {
2799                        result.push(item);
2800                    }
2801                }
2802                Ok(VmValue::List(Box::new(result)))
2803            }
2804            BuiltinId::Reduce | BuiltinId::Fold => {
2805                if args.len() != 3 {
2806                    return Err(runtime_err(
2807                        "reduce()/fold() expects 3 arguments (list, init, fn)",
2808                    ));
2809                }
2810                let items = match &args[0] {
2811                    VmValue::List(items) => (**items).clone(),
2812                    _ => return Err(runtime_err("reduce() first arg must be a list")),
2813                };
2814                let mut acc = args[1].clone();
2815                let func = args[2].clone();
2816                for item in items {
2817                    acc = self.call_vm_function(&func, &[acc, item])?;
2818                }
2819                Ok(acc)
2820            }
2821            BuiltinId::Sum => {
2822                if args.len() != 1 {
2823                    return Err(runtime_err("sum() expects 1 argument (list)"));
2824                }
2825                let items = match &args[0] {
2826                    VmValue::List(items) => items,
2827                    _ => return Err(runtime_err("sum() expects a list")),
2828                };
2829                // Check if any floats are present
2830                let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2831                #[cfg(feature = "native")]
2832                if items.len() >= PARALLEL_THRESHOLD {
2833                    // Parallel sum for large lists
2834                    if has_float {
2835                        let total: f64 = items
2836                            .par_iter()
2837                            .map(|v| match v {
2838                                VmValue::Int(n) => *n as f64,
2839                                VmValue::Float(n) => *n,
2840                                _ => 0.0,
2841                            })
2842                            .sum();
2843                        return Ok(VmValue::Float(total));
2844                    } else {
2845                        let total: i64 = items
2846                            .par_iter()
2847                            .map(|v| match v {
2848                                VmValue::Int(n) => *n,
2849                                _ => 0,
2850                            })
2851                            .sum();
2852                        return Ok(VmValue::Int(total));
2853                    }
2854                }
2855                // Sequential path for smaller lists
2856                let mut total: i64 = 0;
2857                let mut is_float = false;
2858                let mut total_f: f64 = 0.0;
2859                for item in items.iter() {
2860                    match item {
2861                        VmValue::Int(n) => {
2862                            if is_float {
2863                                total_f += *n as f64;
2864                            } else {
2865                                total += n;
2866                            }
2867                        }
2868                        VmValue::Float(n) => {
2869                            if !is_float {
2870                                total_f = total as f64;
2871                                is_float = true;
2872                            }
2873                            total_f += n;
2874                        }
2875                        _ => return Err(runtime_err("sum() list must contain numbers")),
2876                    }
2877                }
2878                if is_float {
2879                    Ok(VmValue::Float(total_f))
2880                } else {
2881                    Ok(VmValue::Int(total))
2882                }
2883            }
2884            BuiltinId::Any => {
2885                if args.len() != 2 {
2886                    return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2887                }
2888                let items = match &args[0] {
2889                    VmValue::List(items) => (**items).clone(),
2890                    _ => return Err(runtime_err("any() first arg must be a list")),
2891                };
2892                let func = args[1].clone();
2893                for item in items {
2894                    let val = self.call_vm_function(&func, &[item])?;
2895                    if val.is_truthy() {
2896                        return Ok(VmValue::Bool(true));
2897                    }
2898                }
2899                Ok(VmValue::Bool(false))
2900            }
2901            BuiltinId::All => {
2902                if args.len() != 2 {
2903                    return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2904                }
2905                let items = match &args[0] {
2906                    VmValue::List(items) => (**items).clone(),
2907                    _ => return Err(runtime_err("all() first arg must be a list")),
2908                };
2909                let func = args[1].clone();
2910                for item in items {
2911                    let val = self.call_vm_function(&func, &[item])?;
2912                    if !val.is_truthy() {
2913                        return Ok(VmValue::Bool(false));
2914                    }
2915                }
2916                Ok(VmValue::Bool(true))
2917            }
2918            // ── Data engine builtins ──
2919            #[cfg(feature = "native")]
2920            BuiltinId::ReadCsv => {
2921                if args.len() != 1 {
2922                    return Err(runtime_err("read_csv() expects 1 argument (path)"));
2923                }
2924                let path = match &args[0] {
2925                    VmValue::String(s) => s.to_string(),
2926                    _ => return Err(runtime_err("read_csv() path must be a string")),
2927                };
2928                match self.engine().read_csv(&path) {
2929                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2930                    Err(e) => {
2931                        let msg = e.to_string();
2932                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2933                            type_name: Arc::from("DataError"),
2934                            variant: Arc::from("ParseError"),
2935                            fields: vec![
2936                                VmValue::String(Arc::from(msg.as_str())),
2937                                VmValue::String(Arc::from(path.as_str())),
2938                            ],
2939                        })));
2940                        Err(runtime_err(msg))
2941                    }
2942                }
2943            }
2944            #[cfg(feature = "native")]
2945            BuiltinId::ReadParquet => {
2946                if args.len() != 1 {
2947                    return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2948                }
2949                let path = match &args[0] {
2950                    VmValue::String(s) => s.to_string(),
2951                    _ => return Err(runtime_err("read_parquet() path must be a string")),
2952                };
2953                match self.engine().read_parquet(&path) {
2954                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2955                    Err(e) => {
2956                        let msg = e.to_string();
2957                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2958                            type_name: Arc::from("DataError"),
2959                            variant: Arc::from("ParseError"),
2960                            fields: vec![
2961                                VmValue::String(Arc::from(msg.as_str())),
2962                                VmValue::String(Arc::from(path.as_str())),
2963                            ],
2964                        })));
2965                        Err(runtime_err(msg))
2966                    }
2967                }
2968            }
2969            #[cfg(feature = "native")]
2970            BuiltinId::WriteCsv => {
2971                if args.len() != 2 {
2972                    return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2973                }
2974                let df = match &args[0] {
2975                    VmValue::Table(t) => t.df.clone(),
2976                    _ => return Err(runtime_err("write_csv() first arg must be a table")),
2977                };
2978                let path = match &args[1] {
2979                    VmValue::String(s) => s.to_string(),
2980                    _ => return Err(runtime_err("write_csv() path must be a string")),
2981                };
2982                match self.engine().write_csv(df, &path) {
2983                    Ok(_) => Ok(VmValue::None),
2984                    Err(e) => {
2985                        let msg = e.to_string();
2986                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2987                            type_name: Arc::from("DataError"),
2988                            variant: Arc::from("ParseError"),
2989                            fields: vec![
2990                                VmValue::String(Arc::from(msg.as_str())),
2991                                VmValue::String(Arc::from(path.as_str())),
2992                            ],
2993                        })));
2994                        Err(runtime_err(msg))
2995                    }
2996                }
2997            }
2998            #[cfg(feature = "native")]
2999            BuiltinId::WriteParquet => {
3000                if args.len() != 2 {
3001                    return Err(runtime_err(
3002                        "write_parquet() expects 2 arguments (table, path)",
3003                    ));
3004                }
3005                let df = match &args[0] {
3006                    VmValue::Table(t) => t.df.clone(),
3007                    _ => return Err(runtime_err("write_parquet() first arg must be a table")),
3008                };
3009                let path = match &args[1] {
3010                    VmValue::String(s) => s.to_string(),
3011                    _ => return Err(runtime_err("write_parquet() path must be a string")),
3012                };
3013                match self.engine().write_parquet(df, &path) {
3014                    Ok(_) => Ok(VmValue::None),
3015                    Err(e) => {
3016                        let msg = e.to_string();
3017                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3018                            type_name: Arc::from("DataError"),
3019                            variant: Arc::from("ParseError"),
3020                            fields: vec![
3021                                VmValue::String(Arc::from(msg.as_str())),
3022                                VmValue::String(Arc::from(path.as_str())),
3023                            ],
3024                        })));
3025                        Err(runtime_err(msg))
3026                    }
3027                }
3028            }
3029            #[cfg(feature = "native")]
3030            BuiltinId::Collect => {
3031                if args.len() != 1 {
3032                    return Err(runtime_err("collect() expects 1 argument (table)"));
3033                }
3034                let df = match &args[0] {
3035                    VmValue::Table(t) => t.df.clone(),
3036                    _ => return Err(runtime_err("collect() expects a table")),
3037                };
3038                let batches = self.engine().collect(df).map_err(runtime_err)?;
3039                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3040                Ok(VmValue::String(Arc::from(formatted.as_str())))
3041            }
3042            #[cfg(feature = "native")]
3043            BuiltinId::ToRows => {
3044                use tl_data::datafusion::arrow::array::{
3045                    Array, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array,
3046                    LargeStringArray, StringArray, UInt32Array, UInt64Array,
3047                };
3048                if args.len() != 1 {
3049                    return Err(runtime_err("to_rows() expects 1 argument (table)"));
3050                }
3051                let df = match &args[0] {
3052                    VmValue::Table(t) => t.df.clone(),
3053                    _ => return Err(runtime_err("to_rows() expects a table")),
3054                };
3055                let batches = self.engine().collect(df).map_err(runtime_err)?;
3056                let mut rows: Vec<VmValue> = Vec::new();
3057                for batch in &batches {
3058                    let schema = batch.schema();
3059                    let num_rows = batch.num_rows();
3060                    for row_idx in 0..num_rows {
3061                        let mut map: Vec<(Arc<str>, VmValue)> = Vec::new();
3062                        for col_idx in 0..batch.num_columns() {
3063                            let col_name: Arc<str> =
3064                                Arc::from(schema.field(col_idx).name().as_str());
3065                            let col = batch.column(col_idx);
3066                            let val = if col.is_null(row_idx) {
3067                                VmValue::None
3068                            } else if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
3069                                VmValue::Float(arr.value(row_idx))
3070                            } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
3071                                VmValue::Float(arr.value(row_idx) as f64)
3072                            } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
3073                                VmValue::Int(arr.value(row_idx))
3074                            } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
3075                                VmValue::Int(arr.value(row_idx) as i64)
3076                            } else if let Some(arr) = col.as_any().downcast_ref::<UInt64Array>() {
3077                                VmValue::Int(arr.value(row_idx) as i64)
3078                            } else if let Some(arr) = col.as_any().downcast_ref::<UInt32Array>() {
3079                                VmValue::Int(arr.value(row_idx) as i64)
3080                            } else if let Some(arr) = col.as_any().downcast_ref::<StringArray>() {
3081                                VmValue::String(Arc::from(arr.value(row_idx)))
3082                            } else if let Some(arr) =
3083                                col.as_any().downcast_ref::<LargeStringArray>()
3084                            {
3085                                VmValue::String(Arc::from(arr.value(row_idx)))
3086                            } else if let Some(arr) = col.as_any().downcast_ref::<BooleanArray>() {
3087                                VmValue::Bool(arr.value(row_idx))
3088                            } else {
3089                                VmValue::String(Arc::from(
3090                                    format!("{:?}", col.data_type()).as_str(),
3091                                ))
3092                            };
3093                            map.push((col_name, val));
3094                        }
3095                        rows.push(VmValue::Map(Box::new(map)));
3096                    }
3097                }
3098                Ok(VmValue::List(Box::new(rows)))
3099            }
3100            #[cfg(feature = "native")]
3101            BuiltinId::Show => {
3102                let df = match args.first() {
3103                    Some(VmValue::Table(t)) => t.df.clone(),
3104                    _ => return Err(runtime_err("show() expects a table")),
3105                };
3106                let limit = match args.get(1) {
3107                    Some(VmValue::Int(n)) => *n as usize,
3108                    None => 20,
3109                    _ => return Err(runtime_err("show() second arg must be an int")),
3110                };
3111                let limited = df
3112                    .limit(0, Some(limit))
3113                    .map_err(|e| runtime_err(format!("{e}")))?;
3114                let batches = self.engine().collect(limited).map_err(runtime_err)?;
3115                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3116                println!("{formatted}");
3117                self.output.push(formatted);
3118                Ok(VmValue::None)
3119            }
3120            #[cfg(feature = "native")]
3121            BuiltinId::Describe => {
3122                if args.len() != 1 {
3123                    return Err(runtime_err("describe() expects 1 argument (table)"));
3124                }
3125                let df = match &args[0] {
3126                    VmValue::Table(t) => t.df.clone(),
3127                    _ => return Err(runtime_err("describe() expects a table")),
3128                };
3129                let schema = df.schema();
3130                let mut lines = Vec::new();
3131                lines.push("Columns:".to_string());
3132                for (qualifier, field) in schema.iter() {
3133                    let prefix = match qualifier {
3134                        Some(q) => format!("{q}."),
3135                        None => String::new(),
3136                    };
3137                    lines.push(format!(
3138                        "  {}{}: {}",
3139                        prefix,
3140                        field.name(),
3141                        field.data_type()
3142                    ));
3143                }
3144                let output = lines.join("\n");
3145                println!("{output}");
3146                self.output.push(output.clone());
3147                Ok(VmValue::String(Arc::from(output.as_str())))
3148            }
3149            #[cfg(feature = "native")]
3150            BuiltinId::Head => {
3151                if args.is_empty() {
3152                    return Err(runtime_err("head() expects at least 1 argument (table)"));
3153                }
3154                let df = match &args[0] {
3155                    VmValue::Table(t) => t.df.clone(),
3156                    _ => return Err(runtime_err("head() first arg must be a table")),
3157                };
3158                let n = match args.get(1) {
3159                    Some(VmValue::Int(n)) => *n as usize,
3160                    None => 10,
3161                    _ => return Err(runtime_err("head() second arg must be an int")),
3162                };
3163                let limited = df
3164                    .limit(0, Some(n))
3165                    .map_err(|e| runtime_err(format!("{e}")))?;
3166                Ok(VmValue::Table(VmTable { df: limited }))
3167            }
3168            #[cfg(feature = "native")]
3169            BuiltinId::Postgres => {
3170                if args.len() != 2 {
3171                    return Err(runtime_err(
3172                        "postgres() expects 2 arguments (conn_str, table_name)",
3173                    ));
3174                }
3175                let conn_str = match &args[0] {
3176                    VmValue::String(s) => s.to_string(),
3177                    _ => return Err(runtime_err("postgres() conn_str must be a string")),
3178                };
3179                let table_name = match &args[1] {
3180                    VmValue::String(s) => s.to_string(),
3181                    _ => return Err(runtime_err("postgres() table_name must be a string")),
3182                };
3183                let conn_str = resolve_tl_config_connection(&conn_str);
3184                match self.engine().read_postgres(&conn_str, &table_name) {
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            #[cfg(feature = "native")]
3201            BuiltinId::WritePostgres => {
3202                if args.len() < 3 {
3203                    return Err(runtime_err(
3204                        "write_postgres() expects (table, conn_str, table_name, [mode])",
3205                    ));
3206                }
3207                // Writes are state-mutating — gate on the sandbox connector policy.
3208                self.check_permission("connector:postgres")?;
3209                let df = match &args[0] {
3210                    VmValue::Table(t) => t.df.clone(),
3211                    _ => return Err(runtime_err("write_postgres() first arg must be a table")),
3212                };
3213                let conn_str = match &args[1] {
3214                    VmValue::String(s) => resolve_tl_config_connection(s),
3215                    _ => return Err(runtime_err("write_postgres() conn_str must be a string")),
3216                };
3217                let table_name = match &args[2] {
3218                    VmValue::String(s) => s.to_string(),
3219                    _ => return Err(runtime_err("write_postgres() table_name must be a string")),
3220                };
3221                let mode = match args.get(3) {
3222                    None | Some(VmValue::None) => "create".to_string(),
3223                    Some(VmValue::String(s)) => s.to_string(),
3224                    _ => return Err(runtime_err("write_postgres() mode must be a string")),
3225                };
3226                let n = self
3227                    .engine()
3228                    .write_postgres(df, &conn_str, &table_name, &mode)
3229                    .map_err(runtime_err)?;
3230                Ok(VmValue::Int(n as i64))
3231            }
3232            #[cfg(feature = "native")]
3233            BuiltinId::WriteRedshift => {
3234                if args.len() < 3 {
3235                    return Err(runtime_err(
3236                        "write_redshift() expects (table, conn_str, table_name, [mode])",
3237                    ));
3238                }
3239                self.check_permission("connector:redshift")?;
3240                let df = match &args[0] {
3241                    VmValue::Table(t) => t.df.clone(),
3242                    _ => return Err(runtime_err("write_redshift() first arg must be a table")),
3243                };
3244                let conn_str = match &args[1] {
3245                    VmValue::String(s) => resolve_tl_config_connection(s),
3246                    _ => return Err(runtime_err("write_redshift() conn_str must be a string")),
3247                };
3248                let table_name = match &args[2] {
3249                    VmValue::String(s) => s.to_string(),
3250                    _ => return Err(runtime_err("write_redshift() table_name must be a string")),
3251                };
3252                let mode = match args.get(3) {
3253                    None | Some(VmValue::None) => "create".to_string(),
3254                    Some(VmValue::String(s)) => s.to_string(),
3255                    _ => return Err(runtime_err("write_redshift() mode must be a string")),
3256                };
3257                let n = self
3258                    .engine()
3259                    .write_redshift(df, &conn_str, &table_name, &mode)
3260                    .map_err(runtime_err)?;
3261                Ok(VmValue::Int(n as i64))
3262            }
3263            #[cfg(feature = "native")]
3264            BuiltinId::WriteMysql => {
3265                #[cfg(feature = "mysql")]
3266                {
3267                    if args.len() < 3 {
3268                        return Err(runtime_err(
3269                            "write_mysql() expects (table, conn_str, table_name, [mode])",
3270                        ));
3271                    }
3272                    self.check_permission("connector:mysql")?;
3273                    let df = match &args[0] {
3274                        VmValue::Table(t) => t.df.clone(),
3275                        _ => return Err(runtime_err("write_mysql() first arg must be a table")),
3276                    };
3277                    let conn_str = match &args[1] {
3278                        VmValue::String(s) => resolve_tl_config_connection(s),
3279                        _ => return Err(runtime_err("write_mysql() conn_str must be a string")),
3280                    };
3281                    let table_name = match &args[2] {
3282                        VmValue::String(s) => s.to_string(),
3283                        _ => return Err(runtime_err("write_mysql() table_name must be a string")),
3284                    };
3285                    let mode = match args.get(3) {
3286                        None | Some(VmValue::None) => "create".to_string(),
3287                        Some(VmValue::String(s)) => s.to_string(),
3288                        _ => return Err(runtime_err("write_mysql() mode must be a string")),
3289                    };
3290                    let n = self
3291                        .engine()
3292                        .write_mysql(df, &conn_str, &table_name, &mode)
3293                        .map_err(runtime_err)?;
3294                    Ok(VmValue::Int(n as i64))
3295                }
3296                #[cfg(not(feature = "mysql"))]
3297                Err(runtime_err("write_mysql() requires the 'mysql' feature"))
3298            }
3299            #[cfg(feature = "native")]
3300            BuiltinId::WriteClickHouse => {
3301                #[cfg(feature = "clickhouse")]
3302                {
3303                    if args.len() < 3 {
3304                        return Err(runtime_err(
3305                            "write_clickhouse() expects (table, url, table_name, [mode])",
3306                        ));
3307                    }
3308                    self.check_permission("connector:clickhouse")?;
3309                    let df = match &args[0] {
3310                        VmValue::Table(t) => t.df.clone(),
3311                        _ => {
3312                            return Err(runtime_err(
3313                                "write_clickhouse() first arg must be a table",
3314                            ));
3315                        }
3316                    };
3317                    let url = match &args[1] {
3318                        VmValue::String(s) => resolve_tl_config_connection(s),
3319                        _ => return Err(runtime_err("write_clickhouse() url must be a string")),
3320                    };
3321                    let table_name = match &args[2] {
3322                        VmValue::String(s) => s.to_string(),
3323                        _ => {
3324                            return Err(runtime_err(
3325                                "write_clickhouse() table_name must be a string",
3326                            ));
3327                        }
3328                    };
3329                    let mode = match args.get(3) {
3330                        None | Some(VmValue::None) => "create".to_string(),
3331                        Some(VmValue::String(s)) => s.to_string(),
3332                        _ => return Err(runtime_err("write_clickhouse() mode must be a string")),
3333                    };
3334                    let n = self
3335                        .engine()
3336                        .write_clickhouse(df, &url, &table_name, &mode)
3337                        .map_err(runtime_err)?;
3338                    Ok(VmValue::Int(n as i64))
3339                }
3340                #[cfg(not(feature = "clickhouse"))]
3341                Err(runtime_err(
3342                    "write_clickhouse() requires the 'clickhouse' feature",
3343                ))
3344            }
3345            #[cfg(feature = "native")]
3346            BuiltinId::WriteSnowflake => {
3347                #[cfg(feature = "snowflake")]
3348                {
3349                    let (df, cfg, table_name, mode) = write_args(&args, "write_snowflake")?;
3350                    self.check_permission("connector:snowflake")?;
3351                    let n = self
3352                        .engine()
3353                        .write_snowflake(df, &cfg, &table_name, &mode)
3354                        .map_err(runtime_err)?;
3355                    Ok(VmValue::Int(n as i64))
3356                }
3357                #[cfg(not(feature = "snowflake"))]
3358                Err(runtime_err(
3359                    "write_snowflake() requires the 'snowflake' feature",
3360                ))
3361            }
3362            #[cfg(feature = "native")]
3363            BuiltinId::WriteBigQuery => {
3364                #[cfg(feature = "bigquery")]
3365                {
3366                    let (df, cfg, table_name, mode) = write_args(&args, "write_bigquery")?;
3367                    self.check_permission("connector:bigquery")?;
3368                    let n = self
3369                        .engine()
3370                        .write_bigquery(df, &cfg, &table_name, &mode)
3371                        .map_err(runtime_err)?;
3372                    Ok(VmValue::Int(n as i64))
3373                }
3374                #[cfg(not(feature = "bigquery"))]
3375                Err(runtime_err(
3376                    "write_bigquery() requires the 'bigquery' feature",
3377                ))
3378            }
3379            #[cfg(feature = "native")]
3380            BuiltinId::WriteDatabricks => {
3381                #[cfg(feature = "databricks")]
3382                {
3383                    let (df, cfg, table_name, mode) = write_args(&args, "write_databricks")?;
3384                    self.check_permission("connector:databricks")?;
3385                    let n = self
3386                        .engine()
3387                        .write_databricks(df, &cfg, &table_name, &mode)
3388                        .map_err(runtime_err)?;
3389                    Ok(VmValue::Int(n as i64))
3390                }
3391                #[cfg(not(feature = "databricks"))]
3392                Err(runtime_err(
3393                    "write_databricks() requires the 'databricks' feature",
3394                ))
3395            }
3396            #[cfg(feature = "native")]
3397            BuiltinId::WriteMssql => {
3398                #[cfg(feature = "mssql")]
3399                {
3400                    if args.len() < 3 {
3401                        return Err(runtime_err(
3402                            "write_mssql() expects (table, conn_str, table_name, [mode])",
3403                        ));
3404                    }
3405                    self.check_permission("connector:mssql")?;
3406                    let df = match &args[0] {
3407                        VmValue::Table(t) => t.df.clone(),
3408                        _ => return Err(runtime_err("write_mssql() first arg must be a table")),
3409                    };
3410                    let conn_str = match &args[1] {
3411                        VmValue::String(s) => resolve_tl_config_connection(s),
3412                        _ => return Err(runtime_err("write_mssql() conn_str must be a string")),
3413                    };
3414                    let table_name = match &args[2] {
3415                        VmValue::String(s) => s.to_string(),
3416                        _ => return Err(runtime_err("write_mssql() table_name must be a string")),
3417                    };
3418                    let mode = match args.get(3) {
3419                        None | Some(VmValue::None) => "create".to_string(),
3420                        Some(VmValue::String(s)) => s.to_string(),
3421                        _ => return Err(runtime_err("write_mssql() mode must be a string")),
3422                    };
3423                    let n = self
3424                        .engine()
3425                        .write_mssql(df, &conn_str, &table_name, &mode)
3426                        .map_err(runtime_err)?;
3427                    Ok(VmValue::Int(n as i64))
3428                }
3429                #[cfg(not(feature = "mssql"))]
3430                Err(runtime_err("write_mssql() requires the 'mssql' feature"))
3431            }
3432            #[cfg(feature = "native")]
3433            BuiltinId::WriteMongo => {
3434                #[cfg(feature = "mongodb")]
3435                {
3436                    if args.len() < 4 {
3437                        return Err(runtime_err(
3438                            "write_mongo() expects (table, conn_str, database, collection, [mode])",
3439                        ));
3440                    }
3441                    self.check_permission("connector:mongodb")?;
3442                    let df = match &args[0] {
3443                        VmValue::Table(t) => t.df.clone(),
3444                        _ => return Err(runtime_err("write_mongo() first arg must be a table")),
3445                    };
3446                    let conn_str = match &args[1] {
3447                        VmValue::String(s) => resolve_tl_config_connection(s),
3448                        _ => return Err(runtime_err("write_mongo() conn_str must be a string")),
3449                    };
3450                    let database = match &args[2] {
3451                        VmValue::String(s) => s.to_string(),
3452                        _ => return Err(runtime_err("write_mongo() database must be a string")),
3453                    };
3454                    let collection = match &args[3] {
3455                        VmValue::String(s) => s.to_string(),
3456                        _ => return Err(runtime_err("write_mongo() collection must be a string")),
3457                    };
3458                    let mode = match args.get(4) {
3459                        None | Some(VmValue::None) => "create".to_string(),
3460                        Some(VmValue::String(s)) => s.to_string(),
3461                        _ => return Err(runtime_err("write_mongo() mode must be a string")),
3462                    };
3463                    let n = self
3464                        .engine()
3465                        .write_mongo(df, &conn_str, &database, &collection, &mode)
3466                        .map_err(runtime_err)?;
3467                    Ok(VmValue::Int(n as i64))
3468                }
3469                #[cfg(not(feature = "mongodb"))]
3470                Err(runtime_err("write_mongo() requires the 'mongodb' feature"))
3471            }
3472            #[cfg(feature = "native")]
3473            BuiltinId::PostgresQuery => {
3474                if args.len() != 2 {
3475                    return Err(runtime_err(
3476                        "postgres_query() expects 2 arguments (conn_str, query)",
3477                    ));
3478                }
3479                let conn_str = match &args[0] {
3480                    VmValue::String(s) => s.to_string(),
3481                    _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3482                };
3483                let query = match &args[1] {
3484                    VmValue::String(s) => s.to_string(),
3485                    _ => return Err(runtime_err("postgres_query() query must be a string")),
3486                };
3487                let conn_str = resolve_tl_config_connection(&conn_str);
3488                match self
3489                    .engine()
3490                    .query_postgres(&conn_str, &query, "__pg_query_result")
3491                {
3492                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
3493                    Err(e) => {
3494                        let msg = e.to_string();
3495                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3496                            type_name: Arc::from("ConnectorError"),
3497                            variant: Arc::from("QueryError"),
3498                            fields: vec![
3499                                VmValue::String(Arc::from(msg.as_str())),
3500                                VmValue::String(Arc::from("postgres")),
3501                            ],
3502                        })));
3503                        Err(runtime_err(msg))
3504                    }
3505                }
3506            }
3507            BuiltinId::TlConfigResolve => {
3508                if args.len() != 1 {
3509                    return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3510                }
3511                let name = match &args[0] {
3512                    VmValue::String(s) => s.to_string(),
3513                    _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3514                };
3515                let resolved = resolve_tl_config_connection(&name);
3516                Ok(VmValue::String(Arc::from(resolved.as_str())))
3517            }
3518            #[cfg(not(feature = "native"))]
3519            BuiltinId::ReadCsv
3520            | BuiltinId::ReadParquet
3521            | BuiltinId::WriteCsv
3522            | BuiltinId::WriteParquet
3523            | BuiltinId::Collect
3524            | BuiltinId::ToRows
3525            | BuiltinId::Show
3526            | BuiltinId::Describe
3527            | BuiltinId::Head
3528            | BuiltinId::Postgres
3529            | BuiltinId::WritePostgres
3530            | BuiltinId::WriteRedshift
3531            | BuiltinId::WriteMysql
3532            | BuiltinId::WriteClickHouse
3533            | BuiltinId::WriteSnowflake
3534            | BuiltinId::WriteBigQuery
3535            | BuiltinId::WriteDatabricks
3536            | BuiltinId::WriteMssql
3537            | BuiltinId::WriteMongo
3538            | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3539            // ── AI builtins ──
3540            #[cfg(feature = "native")]
3541            BuiltinId::Tensor => {
3542                if args.is_empty() {
3543                    return Err(runtime_err("tensor() expects at least 1 argument"));
3544                }
3545                let data = self.vmvalue_to_f64_list(&args[0])?;
3546                let shape = if args.len() > 1 {
3547                    self.vmvalue_to_usize_list(&args[1])?
3548                } else {
3549                    vec![data.len()]
3550                };
3551                let t = tl_ai::TlTensor::from_vec(data, &shape)
3552                    .map_err(|e| runtime_err(e.to_string()))?;
3553                Ok(VmValue::Tensor(Arc::new(t)))
3554            }
3555            #[cfg(feature = "native")]
3556            BuiltinId::TensorZeros => {
3557                if args.is_empty() {
3558                    return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3559                }
3560                let shape = self.vmvalue_to_usize_list(&args[0])?;
3561                let t = tl_ai::TlTensor::zeros(&shape);
3562                Ok(VmValue::Tensor(Arc::new(t)))
3563            }
3564            #[cfg(feature = "native")]
3565            BuiltinId::TensorOnes => {
3566                if args.is_empty() {
3567                    return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3568                }
3569                let shape = self.vmvalue_to_usize_list(&args[0])?;
3570                let t = tl_ai::TlTensor::ones(&shape);
3571                Ok(VmValue::Tensor(Arc::new(t)))
3572            }
3573            #[cfg(feature = "native")]
3574            BuiltinId::TensorShape => match args.first() {
3575                Some(VmValue::Tensor(t)) => {
3576                    let shape: Vec<VmValue> =
3577                        t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3578                    Ok(VmValue::List(Box::new(shape)))
3579                }
3580                _ => Err(runtime_err("tensor_shape() expects a tensor")),
3581            },
3582            #[cfg(feature = "native")]
3583            BuiltinId::TensorReshape => {
3584                if args.len() != 2 {
3585                    return Err(runtime_err(
3586                        "tensor_reshape() expects 2 arguments (tensor, shape)",
3587                    ));
3588                }
3589                let t = match &args[0] {
3590                    VmValue::Tensor(t) => (**t).clone(),
3591                    _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3592                };
3593                let shape = self.vmvalue_to_usize_list(&args[1])?;
3594                let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3595                Ok(VmValue::Tensor(Arc::new(reshaped)))
3596            }
3597            #[cfg(feature = "native")]
3598            BuiltinId::TensorTranspose => match args.first() {
3599                Some(VmValue::Tensor(t)) => {
3600                    let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3601                    Ok(VmValue::Tensor(Arc::new(transposed)))
3602                }
3603                _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3604            },
3605            #[cfg(feature = "native")]
3606            BuiltinId::TensorSum => match args.first() {
3607                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3608                _ => Err(runtime_err("tensor_sum() expects a tensor")),
3609            },
3610            #[cfg(feature = "native")]
3611            BuiltinId::TensorMean => match args.first() {
3612                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3613                _ => Err(runtime_err("tensor_mean() expects a tensor")),
3614            },
3615            #[cfg(feature = "native")]
3616            BuiltinId::TensorDot => {
3617                if args.len() != 2 {
3618                    return Err(runtime_err("tensor_dot() expects 2 arguments"));
3619                }
3620                let a_t = match &args[0] {
3621                    VmValue::Tensor(t) => t,
3622                    _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3623                };
3624                let b_t = match &args[1] {
3625                    VmValue::Tensor(t) => t,
3626                    _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3627                };
3628                let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3629                Ok(VmValue::Tensor(Arc::new(result)))
3630            }
3631            #[cfg(feature = "native")]
3632            BuiltinId::Predict => {
3633                if args.len() < 2 {
3634                    return Err(runtime_err(
3635                        "predict() expects at least 2 arguments (model, input)",
3636                    ));
3637                }
3638                let model = match &args[0] {
3639                    VmValue::Model(m) => (**m).clone(),
3640                    _ => return Err(runtime_err("predict() first arg must be a model")),
3641                };
3642                let input = match &args[1] {
3643                    VmValue::Tensor(t) => (**t).clone(),
3644                    _ => return Err(runtime_err("predict() second arg must be a tensor")),
3645                };
3646                let result =
3647                    tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3648                Ok(VmValue::Tensor(Arc::new(result)))
3649            }
3650            #[cfg(feature = "native")]
3651            BuiltinId::Similarity => {
3652                if args.len() != 2 {
3653                    return Err(runtime_err("similarity() expects 2 arguments"));
3654                }
3655                let a_t = match &args[0] {
3656                    VmValue::Tensor(t) => t,
3657                    _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3658                };
3659                let b_t = match &args[1] {
3660                    VmValue::Tensor(t) => t,
3661                    _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3662                };
3663                let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3664                Ok(VmValue::Float(sim))
3665            }
3666            #[cfg(feature = "native")]
3667            BuiltinId::AiComplete => {
3668                if args.is_empty() {
3669                    return Err(runtime_err(
3670                        "ai_complete() expects at least 1 argument (prompt)",
3671                    ));
3672                }
3673                let prompt = match &args[0] {
3674                    VmValue::String(s) => s.to_string(),
3675                    _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3676                };
3677                let model = match args.get(1) {
3678                    Some(VmValue::String(s)) => Some(s.to_string()),
3679                    _ => None,
3680                };
3681                let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3682                    .map_err(|e| runtime_err(e.to_string()))?;
3683                Ok(VmValue::String(Arc::from(result.as_str())))
3684            }
3685            #[cfg(feature = "native")]
3686            BuiltinId::AiChat => {
3687                if args.is_empty() {
3688                    return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3689                }
3690                let model = match &args[0] {
3691                    VmValue::String(s) => s.to_string(),
3692                    _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3693                };
3694                let system = match args.get(1) {
3695                    Some(VmValue::String(s)) => Some(s.to_string()),
3696                    _ => None,
3697                };
3698                let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3699                {
3700                    msgs.chunks(2)
3701                        .filter_map(|chunk| {
3702                            if chunk.len() == 2
3703                                && let (VmValue::String(role), VmValue::String(content)) =
3704                                    (&chunk[0], &chunk[1])
3705                            {
3706                                return Some((role.to_string(), content.to_string()));
3707                            }
3708                            None
3709                        })
3710                        .collect()
3711                } else {
3712                    Vec::new()
3713                };
3714                let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3715                    .map_err(|e| runtime_err(e.to_string()))?;
3716                Ok(VmValue::String(Arc::from(result.as_str())))
3717            }
3718            #[cfg(feature = "native")]
3719            BuiltinId::ModelSave => {
3720                if args.len() != 2 {
3721                    return Err(runtime_err(
3722                        "model_save() expects 2 arguments (model, path)",
3723                    ));
3724                }
3725                let model = match &args[0] {
3726                    VmValue::Model(m) => m,
3727                    _ => return Err(runtime_err("model_save() first arg must be a model")),
3728                };
3729                let path = match &args[1] {
3730                    VmValue::String(s) => s.to_string(),
3731                    _ => return Err(runtime_err("model_save() second arg must be a string path")),
3732                };
3733                model
3734                    .save(std::path::Path::new(&path))
3735                    .map_err(|e| runtime_err(e.to_string()))?;
3736                Ok(VmValue::None)
3737            }
3738            #[cfg(feature = "native")]
3739            BuiltinId::ModelLoad => {
3740                if args.is_empty() {
3741                    return Err(runtime_err("model_load() expects 1 argument (path)"));
3742                }
3743                let path = match &args[0] {
3744                    VmValue::String(s) => s.to_string(),
3745                    _ => return Err(runtime_err("model_load() arg must be a string path")),
3746                };
3747                let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3748                    .map_err(|e| runtime_err(e.to_string()))?;
3749                Ok(VmValue::Model(Arc::new(model)))
3750            }
3751            #[cfg(feature = "native")]
3752            BuiltinId::ModelRegister => {
3753                if args.len() != 2 {
3754                    return Err(runtime_err(
3755                        "model_register() expects 2 arguments (name, model)",
3756                    ));
3757                }
3758                let name = match &args[0] {
3759                    VmValue::String(s) => s.to_string(),
3760                    _ => return Err(runtime_err("model_register() first arg must be a string")),
3761                };
3762                let model = match &args[1] {
3763                    VmValue::Model(m) => (**m).clone(),
3764                    _ => return Err(runtime_err("model_register() second arg must be a model")),
3765                };
3766                let registry = tl_ai::ModelRegistry::default_location();
3767                registry
3768                    .register(&name, &model)
3769                    .map_err(|e| runtime_err(e.to_string()))?;
3770                Ok(VmValue::None)
3771            }
3772            #[cfg(feature = "native")]
3773            BuiltinId::ModelList => {
3774                let registry = tl_ai::ModelRegistry::default_location();
3775                let names = registry.list();
3776                let items: Vec<VmValue> = names
3777                    .into_iter()
3778                    .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3779                    .collect();
3780                Ok(VmValue::List(Box::new(items)))
3781            }
3782            #[cfg(feature = "native")]
3783            BuiltinId::ModelGet => {
3784                if args.is_empty() {
3785                    return Err(runtime_err("model_get() expects 1 argument (name)"));
3786                }
3787                let name = match &args[0] {
3788                    VmValue::String(s) => s.to_string(),
3789                    _ => return Err(runtime_err("model_get() arg must be a string")),
3790                };
3791                let registry = tl_ai::ModelRegistry::default_location();
3792                match registry.get(&name) {
3793                    Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3794                    Err(_) => Ok(VmValue::None),
3795                }
3796            }
3797            #[cfg(not(feature = "native"))]
3798            BuiltinId::Tensor
3799            | BuiltinId::TensorZeros
3800            | BuiltinId::TensorOnes
3801            | BuiltinId::TensorShape
3802            | BuiltinId::TensorReshape
3803            | BuiltinId::TensorTranspose
3804            | BuiltinId::TensorSum
3805            | BuiltinId::TensorMean
3806            | BuiltinId::TensorDot
3807            | BuiltinId::Predict
3808            | BuiltinId::Similarity
3809            | BuiltinId::AiComplete
3810            | BuiltinId::AiChat
3811            | BuiltinId::ModelSave
3812            | BuiltinId::ModelLoad
3813            | BuiltinId::ModelRegister
3814            | BuiltinId::ModelList
3815            | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3816            // Streaming builtins
3817            #[cfg(feature = "native")]
3818            BuiltinId::AlertSlack => {
3819                if args.len() < 2 {
3820                    return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3821                }
3822                let url = match &args[0] {
3823                    VmValue::String(s) => s.to_string(),
3824                    _ => return Err(runtime_err("alert_slack: url must be a string")),
3825                };
3826                let msg = format!("{}", args[1]);
3827                tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3828                    .map_err(|e| runtime_err(&e))?;
3829                Ok(VmValue::None)
3830            }
3831            #[cfg(feature = "native")]
3832            BuiltinId::AlertWebhook => {
3833                if args.len() < 2 {
3834                    return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3835                }
3836                let url = match &args[0] {
3837                    VmValue::String(s) => s.to_string(),
3838                    _ => return Err(runtime_err("alert_webhook: url must be a string")),
3839                };
3840                let msg = format!("{}", args[1]);
3841                tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3842                    .map_err(|e| runtime_err(&e))?;
3843                Ok(VmValue::None)
3844            }
3845            #[cfg(feature = "native")]
3846            BuiltinId::Emit => {
3847                if args.is_empty() {
3848                    return Err(runtime_err("emit() requires at least 1 argument"));
3849                }
3850                self.output.push(format!("emit: {}", args[0]));
3851                Ok(args[0].clone())
3852            }
3853            #[cfg(feature = "native")]
3854            BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3855            #[cfg(feature = "native")]
3856            BuiltinId::RunPipeline => {
3857                if args.is_empty() {
3858                    return Err(runtime_err("run_pipeline() requires a pipeline"));
3859                }
3860                if let VmValue::PipelineDef(ref def) = args[0] {
3861                    Ok(VmValue::String(Arc::from(
3862                        format!("Pipeline '{}' triggered", def.name).as_str(),
3863                    )))
3864                } else {
3865                    Err(runtime_err("run_pipeline: argument must be a pipeline"))
3866                }
3867            }
3868            #[cfg(not(feature = "native"))]
3869            BuiltinId::AlertSlack
3870            | BuiltinId::AlertWebhook
3871            | BuiltinId::Emit
3872            | BuiltinId::Lineage
3873            | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3874            // Phase 5: Math builtins
3875            BuiltinId::Sqrt => match args.first() {
3876                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3877                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3878                _ => Err(runtime_err("sqrt() expects a number")),
3879            },
3880            BuiltinId::Pow => {
3881                if args.len() == 2 {
3882                    match (&args[0], &args[1]) {
3883                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3884                        (VmValue::Int(a), VmValue::Int(b)) => {
3885                            Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3886                        }
3887                        (VmValue::Float(a), VmValue::Int(b)) => {
3888                            Ok(VmValue::Float(a.powf(*b as f64)))
3889                        }
3890                        (VmValue::Int(a), VmValue::Float(b)) => {
3891                            Ok(VmValue::Float((*a as f64).powf(*b)))
3892                        }
3893                        _ => Err(runtime_err("pow() expects two numbers")),
3894                    }
3895                } else {
3896                    Err(runtime_err("pow() expects 2 arguments"))
3897                }
3898            }
3899            BuiltinId::Floor => match args.first() {
3900                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3901                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3902                _ => Err(runtime_err("floor() expects a number")),
3903            },
3904            BuiltinId::Ceil => match args.first() {
3905                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3906                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3907                _ => Err(runtime_err("ceil() expects a number")),
3908            },
3909            BuiltinId::Round => match args.first() {
3910                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3911                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3912                _ => Err(runtime_err("round() expects a number")),
3913            },
3914            BuiltinId::Sin => match args.first() {
3915                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3916                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3917                _ => Err(runtime_err("sin() expects a number")),
3918            },
3919            BuiltinId::Cos => match args.first() {
3920                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3921                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3922                _ => Err(runtime_err("cos() expects a number")),
3923            },
3924            BuiltinId::Tan => match args.first() {
3925                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3926                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3927                _ => Err(runtime_err("tan() expects a number")),
3928            },
3929            BuiltinId::Log => match args.first() {
3930                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3931                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3932                _ => Err(runtime_err("log() expects a number")),
3933            },
3934            BuiltinId::Log2 => match args.first() {
3935                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3936                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3937                _ => Err(runtime_err("log2() expects a number")),
3938            },
3939            BuiltinId::Log10 => match args.first() {
3940                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3941                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3942                _ => Err(runtime_err("log10() expects a number")),
3943            },
3944            BuiltinId::Join => {
3945                if args.len() == 2 {
3946                    if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3947                        let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3948                        Ok(VmValue::String(Arc::from(
3949                            parts.join(sep.as_ref()).as_str(),
3950                        )))
3951                    } else {
3952                        Err(runtime_err("join() expects separator and list"))
3953                    }
3954                } else {
3955                    Err(runtime_err("join() expects 2 arguments"))
3956                }
3957            }
3958            #[cfg(feature = "native")]
3959            BuiltinId::HttpGet => {
3960                self.check_permission("network")?;
3961                if args.is_empty() {
3962                    return Err(runtime_err("http_get() expects a URL"));
3963                }
3964                if let VmValue::String(url) = &args[0] {
3965                    match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3966                        Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3967                        Err(e) => {
3968                            let msg = format!("HTTP GET error: {e}");
3969                            self.thrown_value =
3970                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3971                                    type_name: Arc::from("NetworkError"),
3972                                    variant: Arc::from("HttpError"),
3973                                    fields: vec![
3974                                        VmValue::String(Arc::from(msg.as_str())),
3975                                        VmValue::String(url.clone()),
3976                                    ],
3977                                })));
3978                            Err(runtime_err(msg))
3979                        }
3980                    }
3981                } else {
3982                    Err(runtime_err("http_get() expects a string URL"))
3983                }
3984            }
3985            #[cfg(feature = "native")]
3986            BuiltinId::HttpPost => {
3987                self.check_permission("network")?;
3988                if args.len() < 2 {
3989                    return Err(runtime_err("http_post() expects URL and body"));
3990                }
3991                if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3992                    let client = reqwest::blocking::Client::new();
3993                    match client
3994                        .post(url.as_ref())
3995                        .header("Content-Type", "application/json")
3996                        .body(body.to_string())
3997                        .send()
3998                        .and_then(|r| r.text())
3999                    {
4000                        Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
4001                        Err(e) => {
4002                            let msg = format!("HTTP POST error: {e}");
4003                            self.thrown_value =
4004                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4005                                    type_name: Arc::from("NetworkError"),
4006                                    variant: Arc::from("HttpError"),
4007                                    fields: vec![
4008                                        VmValue::String(Arc::from(msg.as_str())),
4009                                        VmValue::String(url.clone()),
4010                                    ],
4011                                })));
4012                            Err(runtime_err(msg))
4013                        }
4014                    }
4015                } else {
4016                    Err(runtime_err("http_post() expects string URL and body"))
4017                }
4018            }
4019            #[cfg(not(feature = "native"))]
4020            BuiltinId::HttpGet | BuiltinId::HttpPost => {
4021                Err(runtime_err("HTTP requests not available in WASM"))
4022            }
4023            BuiltinId::Assert => {
4024                if args.is_empty() {
4025                    return Err(runtime_err("assert() expects at least 1 argument"));
4026                }
4027                if !args[0].is_truthy() {
4028                    let msg = if args.len() > 1 {
4029                        format!("{}", args[1])
4030                    } else {
4031                        "Assertion failed".to_string()
4032                    };
4033                    Err(runtime_err(msg))
4034                } else {
4035                    Ok(VmValue::None)
4036                }
4037            }
4038            BuiltinId::AssertEq => {
4039                if args.len() < 2 {
4040                    return Err(runtime_err("assert_eq() expects 2 arguments"));
4041                }
4042                let eq = match (&args[0], &args[1]) {
4043                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
4044                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
4045                    (VmValue::String(a), VmValue::String(b)) => a == b,
4046                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
4047                    (VmValue::None, VmValue::None) => true,
4048                    _ => false,
4049                };
4050                if !eq {
4051                    Err(runtime_err(format!(
4052                        "Assertion failed: {} != {}",
4053                        args[0], args[1]
4054                    )))
4055                } else {
4056                    Ok(VmValue::None)
4057                }
4058            }
4059            // ── Phase 6: Stdlib & Ecosystem builtins ──
4060            BuiltinId::JsonParse => {
4061                if args.is_empty() {
4062                    return Err(runtime_err("json_parse() expects a string"));
4063                }
4064                if let VmValue::String(s) = &args[0] {
4065                    let json_val: serde_json::Value = serde_json::from_str(s)
4066                        .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
4067                    Ok(vm_json_to_value(&json_val))
4068                } else {
4069                    Err(runtime_err("json_parse() expects a string"))
4070                }
4071            }
4072            BuiltinId::JsonStringify => {
4073                if args.is_empty() {
4074                    return Err(runtime_err("json_stringify() expects a value"));
4075                }
4076                let json = vm_value_to_json(&args[0]);
4077                Ok(VmValue::String(Arc::from(json.to_string().as_str())))
4078            }
4079            BuiltinId::MapFrom => {
4080                if !args.len().is_multiple_of(2) {
4081                    return Err(runtime_err(
4082                        "map_from() expects even number of arguments (key, value pairs)",
4083                    ));
4084                }
4085                let mut pairs = Vec::new();
4086                for chunk in args.chunks(2) {
4087                    let key = match &chunk[0] {
4088                        VmValue::String(s) => s.clone(),
4089                        other => Arc::from(format!("{other}").as_str()),
4090                    };
4091                    pairs.push((key, chunk[1].clone()));
4092                }
4093                Ok(VmValue::Map(Box::new(pairs)))
4094            }
4095            #[cfg(feature = "native")]
4096            BuiltinId::ReadFile => {
4097                self.check_permission("file_read")?;
4098                if args.is_empty() {
4099                    return Err(runtime_err("read_file() expects a path"));
4100                }
4101                if let VmValue::String(path) = &args[0] {
4102                    let content = std::fs::read_to_string(path.as_ref())
4103                        .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
4104                    Ok(VmValue::String(Arc::from(content.as_str())))
4105                } else {
4106                    Err(runtime_err("read_file() expects a string path"))
4107                }
4108            }
4109            #[cfg(feature = "native")]
4110            BuiltinId::WriteFile => {
4111                self.check_permission("file_write")?;
4112                if args.len() < 2 {
4113                    return Err(runtime_err("write_file() expects path and content"));
4114                }
4115                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
4116                    std::fs::write(path.as_ref(), content.as_ref())
4117                        .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
4118                    Ok(VmValue::None)
4119                } else {
4120                    Err(runtime_err("write_file() expects string path and content"))
4121                }
4122            }
4123            #[cfg(feature = "native")]
4124            BuiltinId::AppendFile => {
4125                self.check_permission("file_write")?;
4126                if args.len() < 2 {
4127                    return Err(runtime_err("append_file() expects path and content"));
4128                }
4129                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
4130                    use std::io::Write;
4131                    let mut file = std::fs::OpenOptions::new()
4132                        .create(true)
4133                        .append(true)
4134                        .open(path.as_ref())
4135                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
4136                    file.write_all(content.as_bytes())
4137                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
4138                    Ok(VmValue::None)
4139                } else {
4140                    Err(runtime_err("append_file() expects string path and content"))
4141                }
4142            }
4143            #[cfg(feature = "native")]
4144            BuiltinId::FileExists => {
4145                self.check_permission("file_read")?;
4146                if args.is_empty() {
4147                    return Err(runtime_err("file_exists() expects a path"));
4148                }
4149                if let VmValue::String(path) = &args[0] {
4150                    Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
4151                } else {
4152                    Err(runtime_err("file_exists() expects a string path"))
4153                }
4154            }
4155            #[cfg(feature = "native")]
4156            BuiltinId::ListDir => {
4157                self.check_permission("file_read")?;
4158                if args.is_empty() {
4159                    return Err(runtime_err("list_dir() expects a path"));
4160                }
4161                if let VmValue::String(path) = &args[0] {
4162                    let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
4163                        .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
4164                        .filter_map(|e| e.ok())
4165                        .map(|e| {
4166                            VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
4167                        })
4168                        .collect();
4169                    Ok(VmValue::List(Box::new(entries)))
4170                } else {
4171                    Err(runtime_err("list_dir() expects a string path"))
4172                }
4173            }
4174            #[cfg(not(feature = "native"))]
4175            BuiltinId::ReadFile
4176            | BuiltinId::WriteFile
4177            | BuiltinId::AppendFile
4178            | BuiltinId::FileExists
4179            | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
4180            #[cfg(feature = "native")]
4181            BuiltinId::EnvGet => {
4182                if args.is_empty() {
4183                    return Err(runtime_err("env_get() expects a name"));
4184                }
4185                if let VmValue::String(name) = &args[0] {
4186                    match std::env::var(name.as_ref()) {
4187                        Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
4188                        Err(_) => Ok(VmValue::None),
4189                    }
4190                } else {
4191                    Err(runtime_err("env_get() expects a string"))
4192                }
4193            }
4194            #[cfg(feature = "native")]
4195            BuiltinId::EnvSet => {
4196                self.check_permission("env_write")?;
4197                if args.len() < 2 {
4198                    return Err(runtime_err("env_set() expects name and value"));
4199                }
4200                if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
4201                    let _guard = env_lock();
4202                    unsafe {
4203                        std::env::set_var(name.as_ref(), val.as_ref());
4204                    }
4205                    Ok(VmValue::None)
4206                } else {
4207                    Err(runtime_err("env_set() expects two strings"))
4208                }
4209            }
4210            #[cfg(not(feature = "native"))]
4211            BuiltinId::EnvGet | BuiltinId::EnvSet => {
4212                Err(runtime_err("Environment variables not available in WASM"))
4213            }
4214            BuiltinId::RegexMatch => {
4215                if args.len() < 2 {
4216                    return Err(runtime_err("regex_match() expects pattern and string"));
4217                }
4218                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
4219                    if pattern.len() > 10_000 {
4220                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4221                    }
4222                    let re = regex::RegexBuilder::new(pattern)
4223                        .size_limit(10_000_000)
4224                        .build()
4225                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4226                    Ok(VmValue::Bool(re.is_match(text)))
4227                } else {
4228                    Err(runtime_err(
4229                        "regex_match() expects string pattern and string",
4230                    ))
4231                }
4232            }
4233            BuiltinId::RegexFind => {
4234                if args.len() < 2 {
4235                    return Err(runtime_err("regex_find() expects pattern and string"));
4236                }
4237                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
4238                    if pattern.len() > 10_000 {
4239                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4240                    }
4241                    let re = regex::RegexBuilder::new(pattern)
4242                        .size_limit(10_000_000)
4243                        .build()
4244                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4245                    let matches: Vec<VmValue> = re
4246                        .find_iter(text)
4247                        .map(|m| VmValue::String(Arc::from(m.as_str())))
4248                        .collect();
4249                    Ok(VmValue::List(Box::new(matches)))
4250                } else {
4251                    Err(runtime_err(
4252                        "regex_find() expects string pattern and string",
4253                    ))
4254                }
4255            }
4256            BuiltinId::RegexReplace => {
4257                if args.len() < 3 {
4258                    return Err(runtime_err(
4259                        "regex_replace() expects pattern, string, replacement",
4260                    ));
4261                }
4262                if let (
4263                    VmValue::String(pattern),
4264                    VmValue::String(text),
4265                    VmValue::String(replacement),
4266                ) = (&args[0], &args[1], &args[2])
4267                {
4268                    if pattern.len() > 10_000 {
4269                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4270                    }
4271                    let re = regex::RegexBuilder::new(pattern)
4272                        .size_limit(10_000_000)
4273                        .build()
4274                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4275                    Ok(VmValue::String(Arc::from(
4276                        re.replace_all(text, replacement.as_ref()).as_ref(),
4277                    )))
4278                } else {
4279                    Err(runtime_err("regex_replace() expects three strings"))
4280                }
4281            }
4282            BuiltinId::Now => {
4283                let ts = chrono::Utc::now().timestamp_millis();
4284                Ok(VmValue::DateTime(ts))
4285            }
4286            BuiltinId::DateFormat => {
4287                if args.len() < 2 {
4288                    return Err(runtime_err(
4289                        "date_format() expects datetime/timestamp and format",
4290                    ));
4291                }
4292                let ts = match &args[0] {
4293                    VmValue::DateTime(ms) => *ms,
4294                    VmValue::Int(ms) => *ms,
4295                    _ => {
4296                        return Err(runtime_err(
4297                            "date_format() expects a datetime or int timestamp",
4298                        ));
4299                    }
4300                };
4301                let fmt = match &args[1] {
4302                    VmValue::String(s) => s,
4303                    _ => return Err(runtime_err("date_format() expects a string format")),
4304                };
4305                use chrono::TimeZone;
4306                let secs = ts / 1000;
4307                let nsecs = ((ts % 1000) * 1_000_000) as u32;
4308                let dt = chrono::Utc
4309                    .timestamp_opt(secs, nsecs)
4310                    .single()
4311                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
4312                Ok(VmValue::String(Arc::from(
4313                    dt.format(fmt.as_ref()).to_string().as_str(),
4314                )))
4315            }
4316            BuiltinId::DateParse => {
4317                if args.len() < 2 {
4318                    return Err(runtime_err("date_parse() expects string and format"));
4319                }
4320                if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
4321                    let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
4322                        .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
4323                    let ts = dt.and_utc().timestamp_millis();
4324                    Ok(VmValue::DateTime(ts))
4325                } else {
4326                    Err(runtime_err("date_parse() expects two strings"))
4327                }
4328            }
4329            BuiltinId::Zip => {
4330                if args.len() < 2 {
4331                    return Err(runtime_err("zip() expects two lists"));
4332                }
4333                if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
4334                    let pairs: Vec<VmValue> = a
4335                        .iter()
4336                        .zip(b.iter())
4337                        .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
4338                        .collect();
4339                    Ok(VmValue::List(Box::new(pairs)))
4340                } else {
4341                    Err(runtime_err("zip() expects two lists"))
4342                }
4343            }
4344            BuiltinId::Enumerate => {
4345                if args.is_empty() {
4346                    return Err(runtime_err("enumerate() expects a list"));
4347                }
4348                if let VmValue::List(items) = &args[0] {
4349                    let pairs: Vec<VmValue> = items
4350                        .iter()
4351                        .enumerate()
4352                        .map(|(i, v)| {
4353                            VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
4354                        })
4355                        .collect();
4356                    Ok(VmValue::List(Box::new(pairs)))
4357                } else {
4358                    Err(runtime_err("enumerate() expects a list"))
4359                }
4360            }
4361            BuiltinId::Bool => {
4362                if args.is_empty() {
4363                    return Err(runtime_err("bool() expects a value"));
4364                }
4365                Ok(VmValue::Bool(args[0].is_truthy()))
4366            }
4367
4368            // Phase 7: Concurrency builtins
4369            #[cfg(feature = "native")]
4370            BuiltinId::Spawn => {
4371                if args.is_empty() {
4372                    return Err(runtime_err("spawn() expects a function argument"));
4373                }
4374                match &args[0] {
4375                    VmValue::Function(closure) => {
4376                        let proto = closure.prototype.clone();
4377                        // Close all upvalues (convert Open → Closed with current values)
4378                        let mut closed_upvalues = Vec::new();
4379                        for uv in &closure.upvalues {
4380                            match uv {
4381                                UpvalueRef::Open { stack_index } => {
4382                                    let val = self.stack[*stack_index].clone();
4383                                    closed_upvalues.push(UpvalueRef::Closed(val));
4384                                }
4385                                UpvalueRef::Closed(v) => {
4386                                    closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4387                                }
4388                            }
4389                        }
4390                        let globals = self.globals.clone();
4391                        let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4392
4393                        std::thread::spawn(move || {
4394                            let mut vm = Vm::new();
4395                            vm.globals = globals;
4396                            let result = vm.execute_closure(&proto, &closed_upvalues);
4397                            let _ = tx.send(result.map_err(|e| match e {
4398                                TlError::Runtime(re) => re.message,
4399                                other => format!("{other}"),
4400                            }));
4401                        });
4402
4403                        Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4404                    }
4405                    _ => Err(runtime_err("spawn() expects a function")),
4406                }
4407            }
4408            #[cfg(feature = "native")]
4409            BuiltinId::Sleep => {
4410                if args.is_empty() {
4411                    return Err(runtime_err("sleep() expects a duration in milliseconds"));
4412                }
4413                match &args[0] {
4414                    VmValue::Int(ms) => {
4415                        std::thread::sleep(Duration::from_millis(*ms as u64));
4416                        Ok(VmValue::None)
4417                    }
4418                    _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4419                }
4420            }
4421            #[cfg(feature = "native")]
4422            BuiltinId::Channel => {
4423                let capacity = match args.first() {
4424                    Some(VmValue::Int(n)) => *n as usize,
4425                    None => 64,
4426                    _ => {
4427                        return Err(runtime_err(
4428                            "channel() expects an optional integer capacity",
4429                        ));
4430                    }
4431                };
4432                Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4433            }
4434            #[cfg(feature = "native")]
4435            BuiltinId::Send => {
4436                if args.len() < 2 {
4437                    return Err(runtime_err("send() expects a channel and a value"));
4438                }
4439                match &args[0] {
4440                    VmValue::Channel(ch) => {
4441                        ch.sender
4442                            .send(args[1].clone())
4443                            .map_err(|_| runtime_err("Channel disconnected"))?;
4444                        Ok(VmValue::None)
4445                    }
4446                    _ => Err(runtime_err("send() expects a channel as first argument")),
4447                }
4448            }
4449            #[cfg(feature = "native")]
4450            BuiltinId::Recv => {
4451                if args.is_empty() {
4452                    return Err(runtime_err("recv() expects a channel"));
4453                }
4454                match &args[0] {
4455                    VmValue::Channel(ch) => {
4456                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4457                        match guard.recv() {
4458                            Ok(val) => Ok(val),
4459                            Err(_) => Ok(VmValue::None),
4460                        }
4461                    }
4462                    _ => Err(runtime_err("recv() expects a channel")),
4463                }
4464            }
4465            #[cfg(feature = "native")]
4466            BuiltinId::TryRecv => {
4467                if args.is_empty() {
4468                    return Err(runtime_err("try_recv() expects a channel"));
4469                }
4470                match &args[0] {
4471                    VmValue::Channel(ch) => {
4472                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4473                        match guard.try_recv() {
4474                            Ok(val) => Ok(val),
4475                            Err(_) => Ok(VmValue::None),
4476                        }
4477                    }
4478                    _ => Err(runtime_err("try_recv() expects a channel")),
4479                }
4480            }
4481            #[cfg(feature = "native")]
4482            BuiltinId::AwaitAll => {
4483                if args.is_empty() {
4484                    return Err(runtime_err("await_all() expects a list of tasks"));
4485                }
4486                match &args[0] {
4487                    VmValue::List(tasks) => {
4488                        let mut results = Vec::with_capacity(tasks.len());
4489                        for task in tasks.iter() {
4490                            match task {
4491                                VmValue::Task(t) => {
4492                                    let rx = {
4493                                        let mut guard =
4494                                            t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4495                                        guard.take()
4496                                    };
4497                                    match rx {
4498                                        Some(receiver) => match receiver.recv() {
4499                                            Ok(Ok(val)) => results.push(val),
4500                                            Ok(Err(e)) => return Err(runtime_err(e)),
4501                                            Err(_) => {
4502                                                return Err(runtime_err(
4503                                                    "Task channel disconnected",
4504                                                ));
4505                                            }
4506                                        },
4507                                        None => return Err(runtime_err("Task already awaited")),
4508                                    }
4509                                }
4510                                other => results.push(other.clone()),
4511                            }
4512                        }
4513                        Ok(VmValue::List(Box::new(results)))
4514                    }
4515                    _ => Err(runtime_err("await_all() expects a list")),
4516                }
4517            }
4518            #[cfg(feature = "native")]
4519            BuiltinId::Pmap => {
4520                if args.len() < 2 {
4521                    return Err(runtime_err("pmap() expects a list and a function"));
4522                }
4523                let items = match &args[0] {
4524                    VmValue::List(items) => (**items).clone(),
4525                    _ => return Err(runtime_err("pmap() expects a list as first argument")),
4526                };
4527                let closure = match &args[1] {
4528                    VmValue::Function(c) => c.clone(),
4529                    _ => return Err(runtime_err("pmap() expects a function as second argument")),
4530                };
4531
4532                // Close all upvalues
4533                let mut closed_upvalues = Vec::new();
4534                for uv in &closure.upvalues {
4535                    match uv {
4536                        UpvalueRef::Open { stack_index } => {
4537                            let val = self.stack[*stack_index].clone();
4538                            closed_upvalues.push(UpvalueRef::Closed(val));
4539                        }
4540                        UpvalueRef::Closed(v) => {
4541                            closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4542                        }
4543                    }
4544                }
4545
4546                let proto = closure.prototype.clone();
4547                let globals = self.globals.clone();
4548
4549                // Spawn one thread per item
4550                let mut handles = Vec::with_capacity(items.len());
4551                for item in items {
4552                    let proto = proto.clone();
4553                    let upvalues = closed_upvalues.clone();
4554                    let globals = globals.clone();
4555                    let handle = std::thread::spawn(move || {
4556                        let mut vm = Vm::new();
4557                        vm.globals = globals;
4558                        vm.execute_closure_with_args(&proto, &upvalues, &[item])
4559                            .map_err(|e| match e {
4560                                TlError::Runtime(re) => re.message,
4561                                other => format!("{other}"),
4562                            })
4563                    });
4564                    handles.push(handle);
4565                }
4566
4567                let mut results = Vec::with_capacity(handles.len());
4568                for handle in handles {
4569                    match handle.join() {
4570                        Ok(Ok(val)) => results.push(val),
4571                        Ok(Err(e)) => return Err(runtime_err(e)),
4572                        Err(_) => return Err(runtime_err("pmap() thread panicked")),
4573                    }
4574                }
4575                Ok(VmValue::List(Box::new(results)))
4576            }
4577            #[cfg(feature = "native")]
4578            BuiltinId::Timeout => {
4579                if args.len() < 2 {
4580                    return Err(runtime_err(
4581                        "timeout() expects a task and a duration in milliseconds",
4582                    ));
4583                }
4584                let ms = match &args[1] {
4585                    VmValue::Int(n) => *n as u64,
4586                    _ => return Err(runtime_err("timeout() expects an integer duration")),
4587                };
4588                match &args[0] {
4589                    VmValue::Task(task) => {
4590                        let rx = {
4591                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4592                            guard.take()
4593                        };
4594                        match rx {
4595                            Some(receiver) => {
4596                                match receiver.recv_timeout(Duration::from_millis(ms)) {
4597                                    Ok(Ok(val)) => Ok(val),
4598                                    Ok(Err(e)) => Err(runtime_err(e)),
4599                                    Err(mpsc::RecvTimeoutError::Timeout) => {
4600                                        Err(runtime_err("Task timed out"))
4601                                    }
4602                                    Err(mpsc::RecvTimeoutError::Disconnected) => {
4603                                        Err(runtime_err("Task channel disconnected"))
4604                                    }
4605                                }
4606                            }
4607                            None => Err(runtime_err("Task already awaited")),
4608                        }
4609                    }
4610                    _ => Err(runtime_err("timeout() expects a task as first argument")),
4611                }
4612            }
4613            #[cfg(not(feature = "native"))]
4614            BuiltinId::Spawn
4615            | BuiltinId::Sleep
4616            | BuiltinId::Channel
4617            | BuiltinId::Send
4618            | BuiltinId::Recv
4619            | BuiltinId::TryRecv
4620            | BuiltinId::AwaitAll
4621            | BuiltinId::Pmap
4622            | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4623            // Phase 8: Iterators & Generators
4624            BuiltinId::Next => {
4625                if args.is_empty() {
4626                    return Err(runtime_err("next() expects a generator"));
4627                }
4628                match &args[0] {
4629                    VmValue::Generator(gen_arc) => {
4630                        let g = gen_arc.clone();
4631                        self.generator_next(&g)
4632                    }
4633                    _ => Err(runtime_err("next() expects a generator")),
4634                }
4635            }
4636            BuiltinId::IsGenerator => {
4637                let val = args.first().unwrap_or(&VmValue::None);
4638                Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4639            }
4640            BuiltinId::Iter => {
4641                if args.is_empty() {
4642                    return Err(runtime_err("iter() expects a list"));
4643                }
4644                match &args[0] {
4645                    VmValue::List(items) => {
4646                        let gn = VmGenerator::new(GeneratorKind::ListIter {
4647                            items: (**items).clone(),
4648                            index: 0,
4649                        });
4650                        Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4651                    }
4652                    _ => Err(runtime_err("iter() expects a list")),
4653                }
4654            }
4655            BuiltinId::Take => {
4656                if args.len() < 2 {
4657                    return Err(runtime_err("take() expects a generator and a count"));
4658                }
4659                let gen_arc = match &args[0] {
4660                    VmValue::Generator(g) => g.clone(),
4661                    _ => return Err(runtime_err("take() expects a generator as first argument")),
4662                };
4663                let n = match &args[1] {
4664                    VmValue::Int(n) => *n as usize,
4665                    _ => return Err(runtime_err("take() expects an integer count")),
4666                };
4667                let gn = VmGenerator::new(GeneratorKind::Take {
4668                    source: gen_arc,
4669                    remaining: n,
4670                });
4671                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4672            }
4673            BuiltinId::Skip_ => {
4674                if args.len() < 2 {
4675                    return Err(runtime_err("skip() expects a generator and a count"));
4676                }
4677                let gen_arc = match &args[0] {
4678                    VmValue::Generator(g) => g.clone(),
4679                    _ => return Err(runtime_err("skip() expects a generator as first argument")),
4680                };
4681                let n = match &args[1] {
4682                    VmValue::Int(n) => *n as usize,
4683                    _ => return Err(runtime_err("skip() expects an integer count")),
4684                };
4685                let gn = VmGenerator::new(GeneratorKind::Skip {
4686                    source: gen_arc,
4687                    remaining: n,
4688                });
4689                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4690            }
4691            BuiltinId::GenCollect => {
4692                if args.is_empty() {
4693                    return Err(runtime_err("gen_collect() expects a generator"));
4694                }
4695                match &args[0] {
4696                    VmValue::Generator(gen_arc) => {
4697                        let g = gen_arc.clone();
4698                        let mut items = Vec::new();
4699                        loop {
4700                            let val = self.generator_next(&g)?;
4701                            if matches!(val, VmValue::None) {
4702                                break;
4703                            }
4704                            items.push(val);
4705                        }
4706                        Ok(VmValue::List(Box::new(items)))
4707                    }
4708                    _ => Err(runtime_err("gen_collect() expects a generator")),
4709                }
4710            }
4711            BuiltinId::GenMap => {
4712                if args.len() < 2 {
4713                    return Err(runtime_err("gen_map() expects a generator and a function"));
4714                }
4715                let gen_arc = match &args[0] {
4716                    VmValue::Generator(g) => g.clone(),
4717                    _ => {
4718                        return Err(runtime_err(
4719                            "gen_map() expects a generator as first argument",
4720                        ));
4721                    }
4722                };
4723                let func = args[1].clone();
4724                let gn = VmGenerator::new(GeneratorKind::Map {
4725                    source: gen_arc,
4726                    func,
4727                });
4728                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4729            }
4730            BuiltinId::GenFilter => {
4731                if args.len() < 2 {
4732                    return Err(runtime_err(
4733                        "gen_filter() expects a generator and a function",
4734                    ));
4735                }
4736                let gen_arc = match &args[0] {
4737                    VmValue::Generator(g) => g.clone(),
4738                    _ => {
4739                        return Err(runtime_err(
4740                            "gen_filter() expects a generator as first argument",
4741                        ));
4742                    }
4743                };
4744                let func = args[1].clone();
4745                let gn = VmGenerator::new(GeneratorKind::Filter {
4746                    source: gen_arc,
4747                    func,
4748                });
4749                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4750            }
4751            BuiltinId::Chain => {
4752                if args.len() < 2 {
4753                    return Err(runtime_err("chain() expects two generators"));
4754                }
4755                let first = match &args[0] {
4756                    VmValue::Generator(g) => g.clone(),
4757                    _ => return Err(runtime_err("chain() expects generators")),
4758                };
4759                let second = match &args[1] {
4760                    VmValue::Generator(g) => g.clone(),
4761                    _ => return Err(runtime_err("chain() expects generators")),
4762                };
4763                let gn = VmGenerator::new(GeneratorKind::Chain {
4764                    first,
4765                    second,
4766                    on_second: false,
4767                });
4768                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4769            }
4770            BuiltinId::GenZip => {
4771                if args.len() < 2 {
4772                    return Err(runtime_err("gen_zip() expects two generators"));
4773                }
4774                let first = match &args[0] {
4775                    VmValue::Generator(g) => g.clone(),
4776                    _ => return Err(runtime_err("gen_zip() expects generators")),
4777                };
4778                let second = match &args[1] {
4779                    VmValue::Generator(g) => g.clone(),
4780                    _ => return Err(runtime_err("gen_zip() expects generators")),
4781                };
4782                let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4783                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4784            }
4785            BuiltinId::GenEnumerate => {
4786                if args.is_empty() {
4787                    return Err(runtime_err("gen_enumerate() expects a generator"));
4788                }
4789                let gen_arc = match &args[0] {
4790                    VmValue::Generator(g) => g.clone(),
4791                    _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4792                };
4793                let gn = VmGenerator::new(GeneratorKind::Enumerate {
4794                    source: gen_arc,
4795                    index: 0,
4796                });
4797                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4798            }
4799            // Phase 10: Result builtins
4800            BuiltinId::Ok => {
4801                let val = if args.is_empty() {
4802                    VmValue::None
4803                } else {
4804                    args[0].clone()
4805                };
4806                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4807                    type_name: Arc::from("Result"),
4808                    variant: Arc::from("Ok"),
4809                    fields: vec![val],
4810                })))
4811            }
4812            BuiltinId::Err_ => {
4813                let val = if args.is_empty() {
4814                    VmValue::String(Arc::from("error"))
4815                } else {
4816                    args[0].clone()
4817                };
4818                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4819                    type_name: Arc::from("Result"),
4820                    variant: Arc::from("Err"),
4821                    fields: vec![val],
4822                })))
4823            }
4824            BuiltinId::IsOk => {
4825                if args.is_empty() {
4826                    return Err(runtime_err("is_ok() expects an argument"));
4827                }
4828                match &args[0] {
4829                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4830                        Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4831                    }
4832                    _ => Ok(VmValue::Bool(false)),
4833                }
4834            }
4835            BuiltinId::IsErr => {
4836                if args.is_empty() {
4837                    return Err(runtime_err("is_err() expects an argument"));
4838                }
4839                match &args[0] {
4840                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4841                        Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4842                    }
4843                    _ => Ok(VmValue::Bool(false)),
4844                }
4845            }
4846            BuiltinId::Unwrap => {
4847                if args.is_empty() {
4848                    return Err(runtime_err("unwrap() expects an argument"));
4849                }
4850                match &args[0] {
4851                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4852                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4853                            Ok(ei.fields[0].clone())
4854                        } else if ei.variant.as_ref() == "Err" {
4855                            let msg = if ei.fields.is_empty() {
4856                                "error".to_string()
4857                            } else {
4858                                format!("{}", ei.fields[0])
4859                            };
4860                            Err(runtime_err(format!("unwrap() called on Err({msg})")))
4861                        } else {
4862                            Ok(VmValue::None)
4863                        }
4864                    }
4865                    VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4866                    other => Ok(other.clone()),
4867                }
4868            }
4869            BuiltinId::SetFrom => {
4870                let list = match args.first() {
4871                    Some(VmValue::List(items)) => items,
4872                    _ => return Err(runtime_err("set_from() expects a list")),
4873                };
4874                if list.is_empty() {
4875                    return Ok(VmValue::Set(Box::default()));
4876                }
4877                let mut result = Vec::new();
4878                for item in list.iter() {
4879                    if !result.iter().any(|x| vm_values_equal(x, item)) {
4880                        result.push(item.clone());
4881                    }
4882                }
4883                Ok(VmValue::Set(Box::new(result)))
4884            }
4885            BuiltinId::SetAdd => {
4886                if args.len() < 2 {
4887                    return Err(runtime_err("set_add() expects 2 arguments"));
4888                }
4889                let val = &args[1];
4890                match &args[0] {
4891                    VmValue::Set(items) => {
4892                        let mut new_items = items.clone();
4893                        if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4894                            new_items.push(val.clone());
4895                        }
4896                        Ok(VmValue::Set(new_items))
4897                    }
4898                    _ => Err(runtime_err("set_add() first argument must be a set")),
4899                }
4900            }
4901            BuiltinId::SetRemove => {
4902                if args.len() < 2 {
4903                    return Err(runtime_err("set_remove() expects 2 arguments"));
4904                }
4905                let val = &args[1];
4906                match &args[0] {
4907                    VmValue::Set(items) => {
4908                        let new_items: Vec<VmValue> = items
4909                            .iter()
4910                            .filter(|x| !vm_values_equal(x, val))
4911                            .cloned()
4912                            .collect();
4913                        Ok(VmValue::Set(Box::new(new_items)))
4914                    }
4915                    _ => Err(runtime_err("set_remove() first argument must be a set")),
4916                }
4917            }
4918            BuiltinId::SetContains => {
4919                if args.len() < 2 {
4920                    return Err(runtime_err("set_contains() expects 2 arguments"));
4921                }
4922                let val = &args[1];
4923                match &args[0] {
4924                    VmValue::Set(items) => {
4925                        Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4926                    }
4927                    _ => Err(runtime_err("set_contains() first argument must be a set")),
4928                }
4929            }
4930            BuiltinId::SetUnion => {
4931                if args.len() < 2 {
4932                    return Err(runtime_err("set_union() expects 2 arguments"));
4933                }
4934                match (&args[0], &args[1]) {
4935                    (VmValue::Set(a), VmValue::Set(b)) => {
4936                        let mut result = a.clone();
4937                        for item in b.iter() {
4938                            if !result.iter().any(|x| vm_values_equal(x, item)) {
4939                                result.push(item.clone());
4940                            }
4941                        }
4942                        Ok(VmValue::Set(result))
4943                    }
4944                    _ => Err(runtime_err("set_union() expects two sets")),
4945                }
4946            }
4947            BuiltinId::SetIntersection => {
4948                if args.len() < 2 {
4949                    return Err(runtime_err("set_intersection() expects 2 arguments"));
4950                }
4951                match (&args[0], &args[1]) {
4952                    (VmValue::Set(a), VmValue::Set(b)) => {
4953                        let result: Vec<VmValue> = a
4954                            .iter()
4955                            .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4956                            .cloned()
4957                            .collect();
4958                        Ok(VmValue::Set(Box::new(result)))
4959                    }
4960                    _ => Err(runtime_err("set_intersection() expects two sets")),
4961                }
4962            }
4963            BuiltinId::SetDifference => {
4964                if args.len() < 2 {
4965                    return Err(runtime_err("set_difference() expects 2 arguments"));
4966                }
4967                match (&args[0], &args[1]) {
4968                    (VmValue::Set(a), VmValue::Set(b)) => {
4969                        let result: Vec<VmValue> = a
4970                            .iter()
4971                            .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4972                            .cloned()
4973                            .collect();
4974                        Ok(VmValue::Set(Box::new(result)))
4975                    }
4976                    _ => Err(runtime_err("set_difference() expects two sets")),
4977                }
4978            }
4979
4980            // ── Phase 15: Data Quality & Connectors ──
4981            #[cfg(feature = "native")]
4982            BuiltinId::FillNull => {
4983                if args.len() < 2 {
4984                    return Err(runtime_err(
4985                        "fill_null() expects (table, column, [strategy], [value])",
4986                    ));
4987                }
4988                let df = match &args[0] {
4989                    VmValue::Table(t) => t.df.clone(),
4990                    _ => return Err(runtime_err("fill_null() first arg must be a table")),
4991                };
4992                let column = match &args[1] {
4993                    VmValue::String(s) => s.to_string(),
4994                    _ => return Err(runtime_err("fill_null() column must be a string")),
4995                };
4996                let strategy = if args.len() > 2 {
4997                    match &args[2] {
4998                        VmValue::String(s) => s.to_string(),
4999                        _ => "value".to_string(),
5000                    }
5001                } else {
5002                    "value".to_string()
5003                };
5004                let fill_value = if args.len() > 3 {
5005                    match &args[3] {
5006                        VmValue::Int(n) => Some(*n as f64),
5007                        VmValue::Float(f) => Some(*f),
5008                        _ => None,
5009                    }
5010                } else if args.len() > 2 && strategy == "value" {
5011                    match &args[2] {
5012                        VmValue::Int(n) => {
5013                            return Ok(VmValue::Table(VmTable {
5014                                df: self
5015                                    .engine()
5016                                    .fill_null(df, &column, "value", Some(*n as f64))
5017                                    .map_err(runtime_err)?,
5018                            }));
5019                        }
5020                        VmValue::Float(f) => {
5021                            return Ok(VmValue::Table(VmTable {
5022                                df: self
5023                                    .engine()
5024                                    .fill_null(df, &column, "value", Some(*f))
5025                                    .map_err(runtime_err)?,
5026                            }));
5027                        }
5028                        _ => None,
5029                    }
5030                } else {
5031                    None
5032                };
5033                let result = self
5034                    .engine()
5035                    .fill_null(df, &column, &strategy, fill_value)
5036                    .map_err(runtime_err)?;
5037                Ok(VmValue::Table(VmTable { df: result }))
5038            }
5039            #[cfg(feature = "native")]
5040            BuiltinId::DropNull => {
5041                if args.len() < 2 {
5042                    return Err(runtime_err("drop_null() expects (table, column)"));
5043                }
5044                let df = match &args[0] {
5045                    VmValue::Table(t) => t.df.clone(),
5046                    _ => return Err(runtime_err("drop_null() first arg must be a table")),
5047                };
5048                let column = match &args[1] {
5049                    VmValue::String(s) => s.to_string(),
5050                    _ => return Err(runtime_err("drop_null() column must be a string")),
5051                };
5052                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
5053                Ok(VmValue::Table(VmTable { df: result }))
5054            }
5055            #[cfg(feature = "native")]
5056            BuiltinId::Dedup => {
5057                if args.is_empty() {
5058                    return Err(runtime_err("dedup() expects (table, [columns...])"));
5059                }
5060                let df = match &args[0] {
5061                    VmValue::Table(t) => t.df.clone(),
5062                    _ => return Err(runtime_err("dedup() first arg must be a table")),
5063                };
5064                let columns: Vec<String> = args[1..]
5065                    .iter()
5066                    .filter_map(|a| {
5067                        if let VmValue::String(s) = a {
5068                            Some(s.to_string())
5069                        } else {
5070                            None
5071                        }
5072                    })
5073                    .collect();
5074                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
5075                Ok(VmValue::Table(VmTable { df: result }))
5076            }
5077            #[cfg(feature = "native")]
5078            BuiltinId::Clamp => {
5079                if args.len() < 4 {
5080                    return Err(runtime_err("clamp() expects (table, column, min, max)"));
5081                }
5082                let df = match &args[0] {
5083                    VmValue::Table(t) => t.df.clone(),
5084                    _ => return Err(runtime_err("clamp() first arg must be a table")),
5085                };
5086                let column = match &args[1] {
5087                    VmValue::String(s) => s.to_string(),
5088                    _ => return Err(runtime_err("clamp() column must be a string")),
5089                };
5090                let min_val = match &args[2] {
5091                    VmValue::Int(n) => *n as f64,
5092                    VmValue::Float(f) => *f,
5093                    _ => return Err(runtime_err("clamp() min must be a number")),
5094                };
5095                let max_val = match &args[3] {
5096                    VmValue::Int(n) => *n as f64,
5097                    VmValue::Float(f) => *f,
5098                    _ => return Err(runtime_err("clamp() max must be a number")),
5099                };
5100                let result = self
5101                    .engine()
5102                    .clamp(df, &column, min_val, max_val)
5103                    .map_err(runtime_err)?;
5104                Ok(VmValue::Table(VmTable { df: result }))
5105            }
5106            #[cfg(feature = "native")]
5107            BuiltinId::DataProfile => {
5108                if args.is_empty() {
5109                    return Err(runtime_err("data_profile() expects (table)"));
5110                }
5111                let df = match &args[0] {
5112                    VmValue::Table(t) => t.df.clone(),
5113                    _ => return Err(runtime_err("data_profile() arg must be a table")),
5114                };
5115                let result = self.engine().data_profile(df).map_err(runtime_err)?;
5116                Ok(VmValue::Table(VmTable { df: result }))
5117            }
5118            #[cfg(feature = "native")]
5119            BuiltinId::RowCount => {
5120                if args.is_empty() {
5121                    return Err(runtime_err("row_count() expects (table)"));
5122                }
5123                let df = match &args[0] {
5124                    VmValue::Table(t) => t.df.clone(),
5125                    _ => return Err(runtime_err("row_count() arg must be a table")),
5126                };
5127                let count = self.engine().row_count(df).map_err(runtime_err)?;
5128                Ok(VmValue::Int(count))
5129            }
5130            #[cfg(feature = "native")]
5131            BuiltinId::NullRate => {
5132                if args.len() < 2 {
5133                    return Err(runtime_err("null_rate() expects (table, column)"));
5134                }
5135                let df = match &args[0] {
5136                    VmValue::Table(t) => t.df.clone(),
5137                    _ => return Err(runtime_err("null_rate() first arg must be a table")),
5138                };
5139                let column = match &args[1] {
5140                    VmValue::String(s) => s.to_string(),
5141                    _ => return Err(runtime_err("null_rate() column must be a string")),
5142                };
5143                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
5144                Ok(VmValue::Float(rate))
5145            }
5146            #[cfg(feature = "native")]
5147            BuiltinId::IsUnique => {
5148                if args.len() < 2 {
5149                    return Err(runtime_err("is_unique() expects (table, column)"));
5150                }
5151                let df = match &args[0] {
5152                    VmValue::Table(t) => t.df.clone(),
5153                    _ => return Err(runtime_err("is_unique() first arg must be a table")),
5154                };
5155                let column = match &args[1] {
5156                    VmValue::String(s) => s.to_string(),
5157                    _ => return Err(runtime_err("is_unique() column must be a string")),
5158                };
5159                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
5160                Ok(VmValue::Bool(unique))
5161            }
5162            #[cfg(not(feature = "native"))]
5163            BuiltinId::FillNull
5164            | BuiltinId::DropNull
5165            | BuiltinId::Dedup
5166            | BuiltinId::Clamp
5167            | BuiltinId::DataProfile
5168            | BuiltinId::RowCount
5169            | BuiltinId::NullRate
5170            | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
5171            #[cfg(feature = "native")]
5172            BuiltinId::IsEmail => {
5173                if args.is_empty() {
5174                    return Err(runtime_err("is_email() expects 1 argument"));
5175                }
5176                let s = match &args[0] {
5177                    VmValue::String(s) => s.to_string(),
5178                    _ => return Err(runtime_err("is_email() arg must be a string")),
5179                };
5180                Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
5181            }
5182            #[cfg(feature = "native")]
5183            BuiltinId::IsUrl => {
5184                if args.is_empty() {
5185                    return Err(runtime_err("is_url() expects 1 argument"));
5186                }
5187                let s = match &args[0] {
5188                    VmValue::String(s) => s.to_string(),
5189                    _ => return Err(runtime_err("is_url() arg must be a string")),
5190                };
5191                Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
5192            }
5193            #[cfg(feature = "native")]
5194            BuiltinId::IsPhone => {
5195                if args.is_empty() {
5196                    return Err(runtime_err("is_phone() expects 1 argument"));
5197                }
5198                let s = match &args[0] {
5199                    VmValue::String(s) => s.to_string(),
5200                    _ => return Err(runtime_err("is_phone() arg must be a string")),
5201                };
5202                Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
5203            }
5204            #[cfg(feature = "native")]
5205            BuiltinId::IsBetween => {
5206                if args.len() < 3 {
5207                    return Err(runtime_err("is_between() expects (value, low, high)"));
5208                }
5209                let val = match &args[0] {
5210                    VmValue::Int(n) => *n as f64,
5211                    VmValue::Float(f) => *f,
5212                    _ => return Err(runtime_err("is_between() value must be a number")),
5213                };
5214                let low = match &args[1] {
5215                    VmValue::Int(n) => *n as f64,
5216                    VmValue::Float(f) => *f,
5217                    _ => return Err(runtime_err("is_between() low must be a number")),
5218                };
5219                let high = match &args[2] {
5220                    VmValue::Int(n) => *n as f64,
5221                    VmValue::Float(f) => *f,
5222                    _ => return Err(runtime_err("is_between() high must be a number")),
5223                };
5224                Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
5225            }
5226            #[cfg(feature = "native")]
5227            BuiltinId::Levenshtein => {
5228                if args.len() < 2 {
5229                    return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
5230                }
5231                let a = match &args[0] {
5232                    VmValue::String(s) => s.to_string(),
5233                    _ => return Err(runtime_err("levenshtein() args must be strings")),
5234                };
5235                let b = match &args[1] {
5236                    VmValue::String(s) => s.to_string(),
5237                    _ => return Err(runtime_err("levenshtein() args must be strings")),
5238                };
5239                Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
5240            }
5241            #[cfg(feature = "native")]
5242            BuiltinId::Soundex => {
5243                if args.is_empty() {
5244                    return Err(runtime_err("soundex() expects 1 argument"));
5245                }
5246                let s = match &args[0] {
5247                    VmValue::String(s) => s.to_string(),
5248                    _ => return Err(runtime_err("soundex() arg must be a string")),
5249                };
5250                Ok(VmValue::String(Arc::from(
5251                    tl_data::validate::soundex(&s).as_str(),
5252                )))
5253            }
5254            #[cfg(not(feature = "native"))]
5255            BuiltinId::IsEmail
5256            | BuiltinId::IsUrl
5257            | BuiltinId::IsPhone
5258            | BuiltinId::IsBetween
5259            | BuiltinId::Levenshtein
5260            | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
5261            #[cfg(feature = "native")]
5262            BuiltinId::ReadMysql => {
5263                #[cfg(feature = "mysql")]
5264                {
5265                    if args.len() < 2 {
5266                        return Err(runtime_err("read_mysql() expects (conn_str, query)"));
5267                    }
5268                    let conn_str = match &args[0] {
5269                        VmValue::String(s) => s.to_string(),
5270                        _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
5271                    };
5272                    let query = match &args[1] {
5273                        VmValue::String(s) => s.to_string(),
5274                        _ => return Err(runtime_err("read_mysql() query must be a string")),
5275                    };
5276                    let df = self
5277                        .engine()
5278                        .read_mysql(&conn_str, &query)
5279                        .map_err(runtime_err)?;
5280                    Ok(VmValue::Table(VmTable { df }))
5281                }
5282                #[cfg(not(feature = "mysql"))]
5283                Err(runtime_err("read_mysql() requires the 'mysql' feature"))
5284            }
5285            #[cfg(feature = "native")]
5286            BuiltinId::ReadSqlite => {
5287                #[cfg(feature = "sqlite")]
5288                {
5289                    if args.len() < 2 {
5290                        return Err(runtime_err("read_sqlite() expects (db_path, query)"));
5291                    }
5292                    let db_path = match &args[0] {
5293                        VmValue::String(s) => s.to_string(),
5294                        _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
5295                    };
5296                    let query = match &args[1] {
5297                        VmValue::String(s) => s.to_string(),
5298                        _ => return Err(runtime_err("read_sqlite() query must be a string")),
5299                    };
5300                    let df = self
5301                        .engine()
5302                        .read_sqlite(&db_path, &query)
5303                        .map_err(runtime_err)?;
5304                    Ok(VmValue::Table(VmTable { df }))
5305                }
5306                #[cfg(not(feature = "sqlite"))]
5307                Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
5308            }
5309            #[cfg(feature = "native")]
5310            BuiltinId::WriteSqlite => {
5311                #[cfg(feature = "sqlite")]
5312                {
5313                    if args.len() < 3 {
5314                        return Err(runtime_err(
5315                            "write_sqlite() expects (table, db_path, table_name)",
5316                        ));
5317                    }
5318                    let df = match &args[0] {
5319                        VmValue::Table(t) => t.df.clone(),
5320                        _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
5321                    };
5322                    let db_path = match &args[1] {
5323                        VmValue::String(s) => s.to_string(),
5324                        _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
5325                    };
5326                    let table_name = match &args[2] {
5327                        VmValue::String(s) => s.to_string(),
5328                        _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
5329                    };
5330                    self.engine()
5331                        .write_sqlite(df, &db_path, &table_name)
5332                        .map_err(runtime_err)?;
5333                    Ok(VmValue::None)
5334                }
5335                #[cfg(not(feature = "sqlite"))]
5336                Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
5337            }
5338            #[cfg(feature = "native")]
5339            BuiltinId::ReadDuckDb => {
5340                #[cfg(feature = "duckdb")]
5341                {
5342                    if args.len() < 2 {
5343                        return Err(runtime_err("duckdb() expects (db_path, query)"));
5344                    }
5345                    let db_path = match &args[0] {
5346                        VmValue::String(s) => s.to_string(),
5347                        _ => return Err(runtime_err("duckdb() db_path must be a string")),
5348                    };
5349                    let query = match &args[1] {
5350                        VmValue::String(s) => s.to_string(),
5351                        _ => return Err(runtime_err("duckdb() query must be a string")),
5352                    };
5353                    let df = self
5354                        .engine()
5355                        .read_duckdb(&db_path, &query)
5356                        .map_err(runtime_err)?;
5357                    Ok(VmValue::Table(VmTable { df }))
5358                }
5359                #[cfg(not(feature = "duckdb"))]
5360                Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5361            }
5362            #[cfg(feature = "native")]
5363            BuiltinId::WriteDuckDb => {
5364                #[cfg(feature = "duckdb")]
5365                {
5366                    if args.len() < 3 {
5367                        return Err(runtime_err(
5368                            "write_duckdb() expects (table, db_path, table_name)",
5369                        ));
5370                    }
5371                    let df = match &args[0] {
5372                        VmValue::Table(t) => t.df.clone(),
5373                        _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5374                    };
5375                    let db_path = match &args[1] {
5376                        VmValue::String(s) => s.to_string(),
5377                        _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5378                    };
5379                    let table_name = match &args[2] {
5380                        VmValue::String(s) => s.to_string(),
5381                        _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5382                    };
5383                    self.engine()
5384                        .write_duckdb(df, &db_path, &table_name)
5385                        .map_err(runtime_err)?;
5386                    Ok(VmValue::None)
5387                }
5388                #[cfg(not(feature = "duckdb"))]
5389                Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5390            }
5391            #[cfg(feature = "native")]
5392            BuiltinId::ReadIceberg => {
5393                #[cfg(feature = "iceberg")]
5394                {
5395                    if args.is_empty() {
5396                        return Err(runtime_err(
5397                            "iceberg() expects (metadata_location, [columns | snapshot_id | props], [snapshot_id])",
5398                        ));
5399                    }
5400                    let location = match &args[0] {
5401                        VmValue::String(s) => s.to_string(),
5402                        _ => {
5403                            return Err(runtime_err(
5404                                "iceberg() metadata_location must be a string",
5405                            ));
5406                        }
5407                    };
5408                    // Second arg is polymorphic:
5409                    //   list  -> column projection
5410                    //   int   -> snapshot_id (time-travel)
5411                    //   map   -> full options { columns, snapshot_id, <props> }
5412                    //            (props = object-store config, e.g. s3.region)
5413                    let mut opts = tl_data::IcebergReadOptions::default();
5414                    match args.get(1) {
5415                        None | Some(VmValue::None) => {}
5416                        Some(VmValue::List(items)) => {
5417                            opts.columns = items.iter().map(|x| format!("{x}")).collect();
5418                        }
5419                        Some(VmValue::Int(n)) => opts.snapshot_id = Some(*n),
5420                        Some(VmValue::Map(entries)) => {
5421                            for (k, v) in entries.iter() {
5422                                match k.as_ref() {
5423                                    "columns" => {
5424                                        if let VmValue::List(items) = v {
5425                                            opts.columns =
5426                                                items.iter().map(|x| format!("{x}")).collect();
5427                                        } else {
5428                                            return Err(runtime_err(
5429                                                "iceberg() 'columns' must be a list",
5430                                            ));
5431                                        }
5432                                    }
5433                                    "snapshot_id" => {
5434                                        if let VmValue::Int(n) = v {
5435                                            opts.snapshot_id = Some(*n);
5436                                        } else {
5437                                            return Err(runtime_err(
5438                                                "iceberg() 'snapshot_id' must be an integer",
5439                                            ));
5440                                        }
5441                                    }
5442                                    _ => opts.props.push((k.to_string(), format!("{v}"))),
5443                                }
5444                            }
5445                        }
5446                        Some(_) => {
5447                            return Err(runtime_err(
5448                                "iceberg() second arg must be a column list, a snapshot_id int, or a props map",
5449                            ));
5450                        }
5451                    }
5452                    // Optional third arg: snapshot_id (when columns given positionally).
5453                    match args.get(2) {
5454                        None | Some(VmValue::None) => {}
5455                        Some(VmValue::Int(n)) => opts.snapshot_id = Some(*n),
5456                        Some(_) => {
5457                            return Err(runtime_err(
5458                                "iceberg() third arg (snapshot_id) must be an integer",
5459                            ));
5460                        }
5461                    }
5462                    let df = self
5463                        .engine()
5464                        .read_iceberg(&location, opts)
5465                        .map_err(runtime_err)?;
5466                    Ok(VmValue::Table(VmTable { df }))
5467                }
5468                #[cfg(not(feature = "iceberg"))]
5469                Err(runtime_err("iceberg() requires the 'iceberg' feature"))
5470            }
5471            #[cfg(feature = "native")]
5472            BuiltinId::IcebergSnapshots | BuiltinId::IcebergSchema => {
5473                #[cfg(feature = "iceberg")]
5474                {
5475                    let fname = if matches!(builtin_id, BuiltinId::IcebergSnapshots) {
5476                        "iceberg_snapshots"
5477                    } else {
5478                        "iceberg_schema"
5479                    };
5480                    let location = match args.first() {
5481                        Some(VmValue::String(s)) => s.to_string(),
5482                        _ => {
5483                            return Err(runtime_err(format!(
5484                                "{fname}() expects (metadata_location, [props_map])"
5485                            )));
5486                        }
5487                    };
5488                    let props: Vec<(String, String)> = match args.get(1) {
5489                        None | Some(VmValue::None) => Vec::new(),
5490                        Some(VmValue::Map(entries)) => entries
5491                            .iter()
5492                            .map(|(k, v)| (k.to_string(), format!("{v}")))
5493                            .collect(),
5494                        Some(_) => {
5495                            return Err(runtime_err(format!("{fname}() props must be a map")));
5496                        }
5497                    };
5498                    let df = if matches!(builtin_id, BuiltinId::IcebergSnapshots) {
5499                        self.engine().iceberg_snapshots(&location, props)
5500                    } else {
5501                        self.engine().iceberg_schema(&location, props)
5502                    }
5503                    .map_err(runtime_err)?;
5504                    Ok(VmValue::Table(VmTable { df }))
5505                }
5506                #[cfg(not(feature = "iceberg"))]
5507                Err(runtime_err(
5508                    "iceberg_snapshots()/iceberg_schema() require the 'iceberg' feature",
5509                ))
5510            }
5511            #[cfg(feature = "native")]
5512            BuiltinId::ReadRedshift => {
5513                if args.len() < 2 {
5514                    return Err(runtime_err("redshift() expects (conn_str, query)"));
5515                }
5516                let conn_str = match &args[0] {
5517                    VmValue::String(s) => {
5518                        let s_str = s.to_string();
5519                        resolve_tl_config_connection(&s_str)
5520                    }
5521                    _ => return Err(runtime_err("redshift() conn_str must be a string")),
5522                };
5523                let query = match &args[1] {
5524                    VmValue::String(s) => s.to_string(),
5525                    _ => return Err(runtime_err("redshift() query must be a string")),
5526                };
5527                let df = self
5528                    .engine()
5529                    .read_redshift(&conn_str, &query)
5530                    .map_err(runtime_err)?;
5531                Ok(VmValue::Table(VmTable { df }))
5532            }
5533            #[cfg(feature = "native")]
5534            BuiltinId::ReadMssql => {
5535                #[cfg(feature = "mssql")]
5536                {
5537                    if args.len() < 2 {
5538                        return Err(runtime_err("mssql() expects (conn_str, query)"));
5539                    }
5540                    let conn_str = match &args[0] {
5541                        VmValue::String(s) => {
5542                            let s_str = s.to_string();
5543                            resolve_tl_config_connection(&s_str)
5544                        }
5545                        _ => return Err(runtime_err("mssql() conn_str must be a string")),
5546                    };
5547                    let query = match &args[1] {
5548                        VmValue::String(s) => s.to_string(),
5549                        _ => return Err(runtime_err("mssql() query must be a string")),
5550                    };
5551                    let df = self
5552                        .engine()
5553                        .read_mssql(&conn_str, &query)
5554                        .map_err(runtime_err)?;
5555                    Ok(VmValue::Table(VmTable { df }))
5556                }
5557                #[cfg(not(feature = "mssql"))]
5558                Err(runtime_err("mssql() requires the 'mssql' feature"))
5559            }
5560            #[cfg(feature = "native")]
5561            BuiltinId::ReadSnowflake => {
5562                #[cfg(feature = "snowflake")]
5563                {
5564                    if args.len() < 2 {
5565                        return Err(runtime_err("snowflake() expects (config, query)"));
5566                    }
5567                    let config = match &args[0] {
5568                        VmValue::String(s) => {
5569                            let s_str = s.to_string();
5570                            resolve_tl_config_connection(&s_str)
5571                        }
5572                        _ => return Err(runtime_err("snowflake() config must be a string")),
5573                    };
5574                    let query = match &args[1] {
5575                        VmValue::String(s) => s.to_string(),
5576                        _ => return Err(runtime_err("snowflake() query must be a string")),
5577                    };
5578                    let df = self
5579                        .engine()
5580                        .read_snowflake(&config, &query)
5581                        .map_err(runtime_err)?;
5582                    Ok(VmValue::Table(VmTable { df }))
5583                }
5584                #[cfg(not(feature = "snowflake"))]
5585                Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5586            }
5587            #[cfg(feature = "native")]
5588            BuiltinId::ReadBigQuery => {
5589                #[cfg(feature = "bigquery")]
5590                {
5591                    if args.len() < 2 {
5592                        return Err(runtime_err("bigquery() expects (config, query)"));
5593                    }
5594                    let config = match &args[0] {
5595                        VmValue::String(s) => {
5596                            let s_str = s.to_string();
5597                            resolve_tl_config_connection(&s_str)
5598                        }
5599                        _ => return Err(runtime_err("bigquery() config must be a string")),
5600                    };
5601                    let query = match &args[1] {
5602                        VmValue::String(s) => s.to_string(),
5603                        _ => return Err(runtime_err("bigquery() query must be a string")),
5604                    };
5605                    let df = self
5606                        .engine()
5607                        .read_bigquery(&config, &query)
5608                        .map_err(runtime_err)?;
5609                    Ok(VmValue::Table(VmTable { df }))
5610                }
5611                #[cfg(not(feature = "bigquery"))]
5612                Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5613            }
5614            #[cfg(feature = "native")]
5615            BuiltinId::ReadDatabricks => {
5616                #[cfg(feature = "databricks")]
5617                {
5618                    if args.len() < 2 {
5619                        return Err(runtime_err("databricks() expects (config, query)"));
5620                    }
5621                    let config = match &args[0] {
5622                        VmValue::String(s) => {
5623                            let s_str = s.to_string();
5624                            resolve_tl_config_connection(&s_str)
5625                        }
5626                        _ => return Err(runtime_err("databricks() config must be a string")),
5627                    };
5628                    let query = match &args[1] {
5629                        VmValue::String(s) => s.to_string(),
5630                        _ => return Err(runtime_err("databricks() query must be a string")),
5631                    };
5632                    let df = self
5633                        .engine()
5634                        .read_databricks(&config, &query)
5635                        .map_err(runtime_err)?;
5636                    Ok(VmValue::Table(VmTable { df }))
5637                }
5638                #[cfg(not(feature = "databricks"))]
5639                Err(runtime_err(
5640                    "databricks() requires the 'databricks' feature",
5641                ))
5642            }
5643            #[cfg(feature = "native")]
5644            BuiltinId::ReadClickHouse => {
5645                #[cfg(feature = "clickhouse")]
5646                {
5647                    if args.len() < 2 {
5648                        return Err(runtime_err("clickhouse() expects (url, query)"));
5649                    }
5650                    let url = match &args[0] {
5651                        VmValue::String(s) => {
5652                            let s_str = s.to_string();
5653                            resolve_tl_config_connection(&s_str)
5654                        }
5655                        _ => return Err(runtime_err("clickhouse() url must be a string")),
5656                    };
5657                    let query = match &args[1] {
5658                        VmValue::String(s) => s.to_string(),
5659                        _ => return Err(runtime_err("clickhouse() query must be a string")),
5660                    };
5661                    let df = self
5662                        .engine()
5663                        .read_clickhouse(&url, &query)
5664                        .map_err(runtime_err)?;
5665                    Ok(VmValue::Table(VmTable { df }))
5666                }
5667                #[cfg(not(feature = "clickhouse"))]
5668                Err(runtime_err(
5669                    "clickhouse() requires the 'clickhouse' feature",
5670                ))
5671            }
5672            #[cfg(feature = "native")]
5673            BuiltinId::ReadMongo => {
5674                #[cfg(feature = "mongodb")]
5675                {
5676                    if args.len() < 4 {
5677                        return Err(runtime_err(
5678                            "mongo() expects (conn_str, database, collection, filter_json)",
5679                        ));
5680                    }
5681                    let conn_str = match &args[0] {
5682                        VmValue::String(s) => {
5683                            let s_str = s.to_string();
5684                            resolve_tl_config_connection(&s_str)
5685                        }
5686                        _ => return Err(runtime_err("mongo() conn_str must be a string")),
5687                    };
5688                    let database = match &args[1] {
5689                        VmValue::String(s) => s.to_string(),
5690                        _ => return Err(runtime_err("mongo() database must be a string")),
5691                    };
5692                    let collection = match &args[2] {
5693                        VmValue::String(s) => s.to_string(),
5694                        _ => return Err(runtime_err("mongo() collection must be a string")),
5695                    };
5696                    let filter_json = match &args[3] {
5697                        VmValue::String(s) => s.to_string(),
5698                        _ => return Err(runtime_err("mongo() filter must be a string")),
5699                    };
5700                    let df = self
5701                        .engine()
5702                        .read_mongo(&conn_str, &database, &collection, &filter_json)
5703                        .map_err(runtime_err)?;
5704                    Ok(VmValue::Table(VmTable { df }))
5705                }
5706                #[cfg(not(feature = "mongodb"))]
5707                Err(runtime_err("mongo() requires the 'mongodb' feature"))
5708            }
5709            #[cfg(feature = "native")]
5710            BuiltinId::SftpDownload => {
5711                #[cfg(feature = "sftp")]
5712                {
5713                    if args.len() < 3 {
5714                        return Err(runtime_err(
5715                            "sftp_download() expects (config, remote_path, local_path)",
5716                        ));
5717                    }
5718                    let config = match &args[0] {
5719                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5720                        _ => return Err(runtime_err("sftp_download() config must be a string")),
5721                    };
5722                    let remote = match &args[1] {
5723                        VmValue::String(s) => s.to_string(),
5724                        _ => {
5725                            return Err(runtime_err(
5726                                "sftp_download() remote_path must be a string",
5727                            ));
5728                        }
5729                    };
5730                    let local = match &args[2] {
5731                        VmValue::String(s) => s.to_string(),
5732                        _ => {
5733                            return Err(runtime_err("sftp_download() local_path must be a string"));
5734                        }
5735                    };
5736                    let result = self
5737                        .engine()
5738                        .sftp_download(&config, &remote, &local)
5739                        .map_err(runtime_err)?;
5740                    Ok(VmValue::String(Arc::from(result.as_str())))
5741                }
5742                #[cfg(not(feature = "sftp"))]
5743                Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5744            }
5745            #[cfg(feature = "native")]
5746            BuiltinId::SftpUpload => {
5747                #[cfg(feature = "sftp")]
5748                {
5749                    if args.len() < 3 {
5750                        return Err(runtime_err(
5751                            "sftp_upload() expects (config, local_path, remote_path)",
5752                        ));
5753                    }
5754                    let config = match &args[0] {
5755                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5756                        _ => return Err(runtime_err("sftp_upload() config must be a string")),
5757                    };
5758                    let local = match &args[1] {
5759                        VmValue::String(s) => s.to_string(),
5760                        _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5761                    };
5762                    let remote = match &args[2] {
5763                        VmValue::String(s) => s.to_string(),
5764                        _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5765                    };
5766                    let result = self
5767                        .engine()
5768                        .sftp_upload(&config, &local, &remote)
5769                        .map_err(runtime_err)?;
5770                    Ok(VmValue::String(Arc::from(result.as_str())))
5771                }
5772                #[cfg(not(feature = "sftp"))]
5773                Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5774            }
5775            #[cfg(feature = "native")]
5776            BuiltinId::SftpList => {
5777                #[cfg(feature = "sftp")]
5778                {
5779                    if args.len() < 2 {
5780                        return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5781                    }
5782                    let config = match &args[0] {
5783                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5784                        _ => return Err(runtime_err("sftp_list() config must be a string")),
5785                    };
5786                    let remote = match &args[1] {
5787                        VmValue::String(s) => s.to_string(),
5788                        _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5789                    };
5790                    let df = self
5791                        .engine()
5792                        .sftp_list(&config, &remote)
5793                        .map_err(runtime_err)?;
5794                    Ok(VmValue::Table(VmTable { df }))
5795                }
5796                #[cfg(not(feature = "sftp"))]
5797                Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5798            }
5799            #[cfg(feature = "native")]
5800            BuiltinId::SftpReadCsv => {
5801                #[cfg(feature = "sftp")]
5802                {
5803                    if args.len() < 2 {
5804                        return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5805                    }
5806                    let config = match &args[0] {
5807                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5808                        _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5809                    };
5810                    let remote = match &args[1] {
5811                        VmValue::String(s) => s.to_string(),
5812                        _ => {
5813                            return Err(runtime_err(
5814                                "sftp_read_csv() remote_path must be a string",
5815                            ));
5816                        }
5817                    };
5818                    let df = self
5819                        .engine()
5820                        .sftp_read_csv(&config, &remote)
5821                        .map_err(runtime_err)?;
5822                    Ok(VmValue::Table(VmTable { df }))
5823                }
5824                #[cfg(not(feature = "sftp"))]
5825                Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5826            }
5827            #[cfg(feature = "native")]
5828            BuiltinId::SftpReadParquet => {
5829                #[cfg(feature = "sftp")]
5830                {
5831                    if args.len() < 2 {
5832                        return Err(runtime_err(
5833                            "sftp_read_parquet() expects (config, remote_path)",
5834                        ));
5835                    }
5836                    let config = match &args[0] {
5837                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5838                        _ => {
5839                            return Err(runtime_err("sftp_read_parquet() config must be a string"));
5840                        }
5841                    };
5842                    let remote = match &args[1] {
5843                        VmValue::String(s) => s.to_string(),
5844                        _ => {
5845                            return Err(runtime_err(
5846                                "sftp_read_parquet() remote_path must be a string",
5847                            ));
5848                        }
5849                    };
5850                    let df = self
5851                        .engine()
5852                        .sftp_read_parquet(&config, &remote)
5853                        .map_err(runtime_err)?;
5854                    Ok(VmValue::Table(VmTable { df }))
5855                }
5856                #[cfg(not(feature = "sftp"))]
5857                Err(runtime_err(
5858                    "sftp_read_parquet() requires the 'sftp' feature",
5859                ))
5860            }
5861            #[cfg(feature = "native")]
5862            BuiltinId::RedisConnect => {
5863                #[cfg(feature = "redis")]
5864                {
5865                    if args.is_empty() {
5866                        return Err(runtime_err("redis_connect() expects (url)"));
5867                    }
5868                    let url = match &args[0] {
5869                        VmValue::String(s) => s.to_string(),
5870                        _ => return Err(runtime_err("redis_connect() url must be a string")),
5871                    };
5872                    let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5873                    Ok(VmValue::String(Arc::from(result.as_str())))
5874                }
5875                #[cfg(not(feature = "redis"))]
5876                Err(runtime_err("redis_connect() requires the 'redis' feature"))
5877            }
5878            #[cfg(feature = "native")]
5879            BuiltinId::RedisGet => {
5880                #[cfg(feature = "redis")]
5881                {
5882                    if args.len() < 2 {
5883                        return Err(runtime_err("redis_get() expects (url, key)"));
5884                    }
5885                    let url = match &args[0] {
5886                        VmValue::String(s) => s.to_string(),
5887                        _ => return Err(runtime_err("redis_get() url must be a string")),
5888                    };
5889                    let key = match &args[1] {
5890                        VmValue::String(s) => s.to_string(),
5891                        _ => return Err(runtime_err("redis_get() key must be a string")),
5892                    };
5893                    match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5894                        Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5895                        None => Ok(VmValue::None),
5896                    }
5897                }
5898                #[cfg(not(feature = "redis"))]
5899                Err(runtime_err("redis_get() requires the 'redis' feature"))
5900            }
5901            #[cfg(feature = "native")]
5902            BuiltinId::RedisSet => {
5903                #[cfg(feature = "redis")]
5904                {
5905                    if args.len() < 3 {
5906                        return Err(runtime_err("redis_set() expects (url, key, value)"));
5907                    }
5908                    let url = match &args[0] {
5909                        VmValue::String(s) => s.to_string(),
5910                        _ => return Err(runtime_err("redis_set() url must be a string")),
5911                    };
5912                    let key = match &args[1] {
5913                        VmValue::String(s) => s.to_string(),
5914                        _ => return Err(runtime_err("redis_set() key must be a string")),
5915                    };
5916                    let value = match &args[2] {
5917                        VmValue::String(s) => s.to_string(),
5918                        _ => format!("{}", &args[2]),
5919                    };
5920                    tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5921                    Ok(VmValue::None)
5922                }
5923                #[cfg(not(feature = "redis"))]
5924                Err(runtime_err("redis_set() requires the 'redis' feature"))
5925            }
5926            #[cfg(feature = "native")]
5927            BuiltinId::RedisDel => {
5928                #[cfg(feature = "redis")]
5929                {
5930                    if args.len() < 2 {
5931                        return Err(runtime_err("redis_del() expects (url, key)"));
5932                    }
5933                    let url = match &args[0] {
5934                        VmValue::String(s) => s.to_string(),
5935                        _ => return Err(runtime_err("redis_del() url must be a string")),
5936                    };
5937                    let key = match &args[1] {
5938                        VmValue::String(s) => s.to_string(),
5939                        _ => return Err(runtime_err("redis_del() key must be a string")),
5940                    };
5941                    let deleted =
5942                        tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5943                    Ok(VmValue::Bool(deleted))
5944                }
5945                #[cfg(not(feature = "redis"))]
5946                Err(runtime_err("redis_del() requires the 'redis' feature"))
5947            }
5948            #[cfg(feature = "native")]
5949            BuiltinId::GraphqlQuery => {
5950                if args.len() < 2 {
5951                    return Err(runtime_err(
5952                        "graphql_query() expects (endpoint, query, [variables])",
5953                    ));
5954                }
5955                let endpoint = match &args[0] {
5956                    VmValue::String(s) => s.to_string(),
5957                    _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5958                };
5959                let query = match &args[1] {
5960                    VmValue::String(s) => s.to_string(),
5961                    _ => return Err(runtime_err("graphql_query() query must be a string")),
5962                };
5963                let variables = if args.len() > 2 {
5964                    vm_value_to_json(&args[2])
5965                } else {
5966                    serde_json::Value::Null
5967                };
5968                let mut body = serde_json::Map::new();
5969                body.insert("query".to_string(), serde_json::Value::String(query));
5970                if !variables.is_null() {
5971                    body.insert("variables".to_string(), variables);
5972                }
5973                let client = reqwest::blocking::Client::new();
5974                let resp = client
5975                    .post(&endpoint)
5976                    .header("Content-Type", "application/json")
5977                    .json(&body)
5978                    .send()
5979                    .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5980                let text = resp
5981                    .text()
5982                    .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5983                let json: serde_json::Value = serde_json::from_str(&text)
5984                    .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5985                Ok(vm_json_to_value(&json))
5986            }
5987            #[cfg(feature = "native")]
5988            BuiltinId::RegisterS3 => {
5989                #[cfg(feature = "s3")]
5990                {
5991                    if args.len() < 2 {
5992                        return Err(runtime_err(
5993                            "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5994                        ));
5995                    }
5996                    let bucket = match &args[0] {
5997                        VmValue::String(s) => s.to_string(),
5998                        _ => return Err(runtime_err("register_s3() bucket must be a string")),
5999                    };
6000                    let region = match &args[1] {
6001                        VmValue::String(s) => s.to_string(),
6002                        _ => return Err(runtime_err("register_s3() region must be a string")),
6003                    };
6004                    let access_key = args.get(2).and_then(|v| {
6005                        if let VmValue::String(s) = v {
6006                            Some(s.to_string())
6007                        } else {
6008                            None
6009                        }
6010                    });
6011                    let secret_key = args.get(3).and_then(|v| {
6012                        if let VmValue::String(s) = v {
6013                            Some(s.to_string())
6014                        } else {
6015                            None
6016                        }
6017                    });
6018                    let endpoint = args.get(4).and_then(|v| {
6019                        if let VmValue::String(s) = v {
6020                            Some(s.to_string())
6021                        } else {
6022                            None
6023                        }
6024                    });
6025                    self.engine()
6026                        .register_s3(
6027                            &bucket,
6028                            &region,
6029                            access_key.as_deref(),
6030                            secret_key.as_deref(),
6031                            endpoint.as_deref(),
6032                        )
6033                        .map_err(runtime_err)?;
6034                    Ok(VmValue::None)
6035                }
6036                #[cfg(not(feature = "s3"))]
6037                Err(runtime_err("register_s3() requires the 's3' feature"))
6038            }
6039            #[cfg(not(feature = "native"))]
6040            BuiltinId::ReadMysql
6041            | BuiltinId::ReadSqlite
6042            | BuiltinId::WriteSqlite
6043            | BuiltinId::ReadDuckDb
6044            | BuiltinId::WriteDuckDb
6045            | BuiltinId::ReadIceberg
6046            | BuiltinId::IcebergSnapshots
6047            | BuiltinId::IcebergSchema
6048            | BuiltinId::ReadRedshift
6049            | BuiltinId::ReadMssql
6050            | BuiltinId::ReadSnowflake
6051            | BuiltinId::ReadBigQuery
6052            | BuiltinId::ReadDatabricks
6053            | BuiltinId::ReadClickHouse
6054            | BuiltinId::ReadMongo
6055            | BuiltinId::SftpDownload
6056            | BuiltinId::SftpUpload
6057            | BuiltinId::SftpList
6058            | BuiltinId::SftpReadCsv
6059            | BuiltinId::SftpReadParquet
6060            | BuiltinId::RedisConnect
6061            | BuiltinId::RedisGet
6062            | BuiltinId::RedisSet
6063            | BuiltinId::RedisDel
6064            | BuiltinId::GraphqlQuery
6065            | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
6066            // Phase 20: Python FFI
6067            BuiltinId::PyImport => {
6068                self.check_permission("python")?;
6069                #[cfg(feature = "python")]
6070                {
6071                    crate::python::py_import_impl(&args)
6072                }
6073                #[cfg(not(feature = "python"))]
6074                Err(runtime_err("py_import() requires the 'python' feature"))
6075            }
6076            BuiltinId::PyCall => {
6077                self.check_permission("python")?;
6078                #[cfg(feature = "python")]
6079                {
6080                    crate::python::py_call_impl(&args)
6081                }
6082                #[cfg(not(feature = "python"))]
6083                Err(runtime_err("py_call() requires the 'python' feature"))
6084            }
6085            BuiltinId::PyEval => {
6086                self.check_permission("python")?;
6087                #[cfg(feature = "python")]
6088                {
6089                    crate::python::py_eval_impl(&args)
6090                }
6091                #[cfg(not(feature = "python"))]
6092                Err(runtime_err("py_eval() requires the 'python' feature"))
6093            }
6094            BuiltinId::PyGetAttr => {
6095                self.check_permission("python")?;
6096                #[cfg(feature = "python")]
6097                {
6098                    crate::python::py_getattr_impl(&args)
6099                }
6100                #[cfg(not(feature = "python"))]
6101                Err(runtime_err("py_getattr() requires the 'python' feature"))
6102            }
6103            BuiltinId::PySetAttr => {
6104                self.check_permission("python")?;
6105                #[cfg(feature = "python")]
6106                {
6107                    crate::python::py_setattr_impl(&args)
6108                }
6109                #[cfg(not(feature = "python"))]
6110                Err(runtime_err("py_setattr() requires the 'python' feature"))
6111            }
6112            BuiltinId::PyToTl => {
6113                #[cfg(feature = "python")]
6114                {
6115                    crate::python::py_to_tl_impl(&args)
6116                }
6117                #[cfg(not(feature = "python"))]
6118                Err(runtime_err("py_to_tl() requires the 'python' feature"))
6119            }
6120
6121            // Phase 21: Schema Evolution builtins
6122            #[cfg(feature = "native")]
6123            BuiltinId::SchemaRegister => {
6124                let name = match args.first() {
6125                    Some(VmValue::String(s)) => s.to_string(),
6126                    _ => {
6127                        return Err(runtime_err(
6128                            "schema_register: first arg must be schema name string",
6129                        ));
6130                    }
6131                };
6132                let version = match args.get(1) {
6133                    Some(VmValue::Int(v)) => *v,
6134                    _ => {
6135                        return Err(runtime_err(
6136                            "schema_register: second arg must be version number",
6137                        ));
6138                    }
6139                };
6140                let fields = match args.get(2) {
6141                    Some(VmValue::Map(pairs)) => {
6142                        let mut arrow_fields = Vec::new();
6143                        for (k, v) in pairs.iter() {
6144                            let fname = k.to_string();
6145                            let ftype = match v {
6146                                VmValue::String(s) => s.to_string(),
6147                                _ => "string".to_string(),
6148                            };
6149                            arrow_fields.push(tl_data::ArrowField::new(
6150                                &fname,
6151                                crate::schema::type_name_to_arrow_pub(&ftype),
6152                                true,
6153                            ));
6154                        }
6155                        arrow_fields
6156                    }
6157                    _ => return Err(runtime_err("schema_register: third arg must be fields map")),
6158                };
6159                let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
6160                self.schema_registry
6161                    .register(
6162                        &name,
6163                        version,
6164                        schema,
6165                        crate::schema::SchemaMetadata::default(),
6166                    )
6167                    .map_err(|e| runtime_err(&e))?;
6168                Ok(VmValue::None)
6169            }
6170            #[cfg(feature = "native")]
6171            BuiltinId::SchemaGet => {
6172                let name = match args.first() {
6173                    Some(VmValue::String(s)) => s.to_string(),
6174                    _ => return Err(runtime_err("schema_get: need name")),
6175                };
6176                let version = match args.get(1) {
6177                    Some(VmValue::Int(v)) => *v,
6178                    _ => return Err(runtime_err("schema_get: need version")),
6179                };
6180                match self.schema_registry.get(&name, version) {
6181                    Some(vs) => {
6182                        let fields: Vec<VmValue> = vs
6183                            .schema
6184                            .fields()
6185                            .iter()
6186                            .map(|f| {
6187                                VmValue::String(std::sync::Arc::from(format!(
6188                                    "{}: {}",
6189                                    f.name(),
6190                                    f.data_type()
6191                                )))
6192                            })
6193                            .collect();
6194                        Ok(VmValue::List(Box::new(fields)))
6195                    }
6196                    None => Ok(VmValue::None),
6197                }
6198            }
6199            #[cfg(feature = "native")]
6200            BuiltinId::SchemaLatest => {
6201                let name = match args.first() {
6202                    Some(VmValue::String(s)) => s.to_string(),
6203                    _ => return Err(runtime_err("schema_latest: need name")),
6204                };
6205                match self.schema_registry.latest(&name) {
6206                    Some(vs) => Ok(VmValue::Int(vs.version)),
6207                    None => Ok(VmValue::None),
6208                }
6209            }
6210            #[cfg(feature = "native")]
6211            BuiltinId::SchemaHistory => {
6212                let name = match args.first() {
6213                    Some(VmValue::String(s)) => s.to_string(),
6214                    _ => return Err(runtime_err("schema_history: need name")),
6215                };
6216                let versions = self.schema_registry.versions(&name);
6217                Ok(VmValue::List(Box::new(
6218                    versions.into_iter().map(VmValue::Int).collect(),
6219                )))
6220            }
6221            #[cfg(feature = "native")]
6222            BuiltinId::SchemaCheck => {
6223                let name = match args.first() {
6224                    Some(VmValue::String(s)) => s.to_string(),
6225                    _ => return Err(runtime_err("schema_check: need name")),
6226                };
6227                let v1 = match args.get(1) {
6228                    Some(VmValue::Int(v)) => *v,
6229                    _ => return Err(runtime_err("schema_check: need v1")),
6230                };
6231                let v2 = match args.get(2) {
6232                    Some(VmValue::Int(v)) => *v,
6233                    _ => return Err(runtime_err("schema_check: need v2")),
6234                };
6235                let mode_str = match args.get(3) {
6236                    Some(VmValue::String(s)) => s.to_string(),
6237                    _ => "backward".to_string(),
6238                };
6239                let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
6240                let issues = self
6241                    .schema_registry
6242                    .check_compatibility(&name, v1, v2, mode);
6243                Ok(VmValue::List(Box::new(
6244                    issues
6245                        .into_iter()
6246                        .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
6247                        .collect(),
6248                )))
6249            }
6250            #[cfg(feature = "native")]
6251            BuiltinId::SchemaDiff => {
6252                let name = match args.first() {
6253                    Some(VmValue::String(s)) => s.to_string(),
6254                    _ => return Err(runtime_err("schema_diff: need name")),
6255                };
6256                let v1 = match args.get(1) {
6257                    Some(VmValue::Int(v)) => *v,
6258                    _ => return Err(runtime_err("schema_diff: need v1")),
6259                };
6260                let v2 = match args.get(2) {
6261                    Some(VmValue::Int(v)) => *v,
6262                    _ => return Err(runtime_err("schema_diff: need v2")),
6263                };
6264                let diffs = self.schema_registry.diff(&name, v1, v2);
6265                Ok(VmValue::List(Box::new(
6266                    diffs
6267                        .into_iter()
6268                        .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
6269                        .collect(),
6270                )))
6271            }
6272            #[cfg(feature = "native")]
6273            BuiltinId::SchemaApplyMigration => {
6274                let name = match args.first() {
6275                    Some(VmValue::String(s)) => s.to_string(),
6276                    _ => return Err(runtime_err("schema_apply_migration: need name")),
6277                };
6278                let from_v = match args.get(1) {
6279                    Some(VmValue::Int(v)) => *v,
6280                    _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
6281                };
6282                let to_v = match args.get(2) {
6283                    Some(VmValue::Int(v)) => *v,
6284                    _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
6285                };
6286                Ok(VmValue::String(std::sync::Arc::from(format!(
6287                    "migration {}:{}->{} applied",
6288                    name, from_v, to_v
6289                ))))
6290            }
6291            #[cfg(feature = "native")]
6292            BuiltinId::SchemaVersions => {
6293                let name = match args.first() {
6294                    Some(VmValue::String(s)) => s.to_string(),
6295                    _ => return Err(runtime_err("schema_versions: need name")),
6296                };
6297                let versions = self.schema_registry.versions(&name);
6298                Ok(VmValue::List(Box::new(
6299                    versions.into_iter().map(VmValue::Int).collect(),
6300                )))
6301            }
6302            #[cfg(feature = "native")]
6303            BuiltinId::SchemaFields => {
6304                let name = match args.first() {
6305                    Some(VmValue::String(s)) => s.to_string(),
6306                    _ => return Err(runtime_err("schema_fields: need name")),
6307                };
6308                let version = match args.get(1) {
6309                    Some(VmValue::Int(v)) => *v,
6310                    _ => return Err(runtime_err("schema_fields: need version")),
6311                };
6312                let fields = self.schema_registry.fields(&name, version);
6313                Ok(VmValue::List(Box::new(
6314                    fields
6315                        .into_iter()
6316                        .map(|(n, t)| {
6317                            VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
6318                        })
6319                        .collect(),
6320                )))
6321            }
6322            #[cfg(not(feature = "native"))]
6323            BuiltinId::SchemaRegister
6324            | BuiltinId::SchemaGet
6325            | BuiltinId::SchemaLatest
6326            | BuiltinId::SchemaHistory
6327            | BuiltinId::SchemaCheck
6328            | BuiltinId::SchemaDiff
6329            | BuiltinId::SchemaApplyMigration
6330            | BuiltinId::SchemaVersions
6331            | BuiltinId::SchemaFields => {
6332                let _ = args;
6333                Err(runtime_err("Schema operations not available in WASM"))
6334            }
6335
6336            // ── Phase 22: Advanced Types ──
6337            BuiltinId::Decimal => {
6338                use std::str::FromStr;
6339                let s = match args.first() {
6340                    Some(VmValue::String(s)) => s.to_string(),
6341                    Some(VmValue::Int(n)) => n.to_string(),
6342                    Some(VmValue::Float(f)) => f.to_string(),
6343                    _ => return Err(runtime_err("decimal(): expected string, int, or float")),
6344                };
6345                let d = rust_decimal::Decimal::from_str(&s)
6346                    .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
6347                Ok(VmValue::Decimal(d))
6348            }
6349
6350            // ── Phase 23: Security ──
6351            BuiltinId::SecretGet => {
6352                let key = match args.first() {
6353                    Some(VmValue::String(s)) => s.to_string(),
6354                    _ => return Err(runtime_err("secret_get: need key")),
6355                };
6356                if let Some(val) = self.secret_vault.get(&key) {
6357                    Ok(VmValue::Secret(Arc::from(val.as_str())))
6358                } else {
6359                    // Fallback to env var TL_SECRET_{KEY}
6360                    let env_key = format!("TL_SECRET_{}", key.to_uppercase());
6361                    match std::env::var(&env_key) {
6362                        Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
6363                        Err(_) => Ok(VmValue::None),
6364                    }
6365                }
6366            }
6367            BuiltinId::SecretSet => {
6368                let key = match args.first() {
6369                    Some(VmValue::String(s)) => s.to_string(),
6370                    _ => return Err(runtime_err("secret_set: need key")),
6371                };
6372                let val = match args.get(1) {
6373                    Some(VmValue::String(s)) => s.to_string(),
6374                    Some(VmValue::Secret(s)) => s.to_string(),
6375                    _ => return Err(runtime_err("secret_set: need value")),
6376                };
6377                self.secret_vault.insert(key, val);
6378                Ok(VmValue::None)
6379            }
6380            BuiltinId::SecretDelete => {
6381                let key = match args.first() {
6382                    Some(VmValue::String(s)) => s.to_string(),
6383                    _ => return Err(runtime_err("secret_delete: need key")),
6384                };
6385                self.secret_vault.remove(&key);
6386                Ok(VmValue::None)
6387            }
6388            BuiltinId::SecretList => {
6389                let keys: Vec<VmValue> = self
6390                    .secret_vault
6391                    .keys()
6392                    .map(|k| VmValue::String(Arc::from(k.as_str())))
6393                    .collect();
6394                Ok(VmValue::List(Box::new(keys)))
6395            }
6396            BuiltinId::CheckPermission => {
6397                let perm = match args.first() {
6398                    Some(VmValue::String(s)) => s.to_string(),
6399                    _ => return Err(runtime_err("check_permission: need permission name")),
6400                };
6401                let allowed = match self.security_policy {
6402                    Some(ref policy) => policy.check(&perm),
6403                    None => true,
6404                };
6405                Ok(VmValue::Bool(allowed))
6406            }
6407            BuiltinId::MaskEmail => {
6408                let email = match args.first() {
6409                    Some(VmValue::String(s)) => s.to_string(),
6410                    _ => return Err(runtime_err("mask_email: need string")),
6411                };
6412                let masked = if let Some(at_pos) = email.find('@') {
6413                    let local = &email[..at_pos];
6414                    let domain = &email[at_pos..];
6415                    if local.len() > 1 {
6416                        format!("{}***{}", &local[..1], domain)
6417                    } else {
6418                        format!("***{domain}")
6419                    }
6420                } else {
6421                    "***".to_string()
6422                };
6423                Ok(VmValue::String(Arc::from(masked.as_str())))
6424            }
6425            BuiltinId::MaskPhone => {
6426                let phone = match args.first() {
6427                    Some(VmValue::String(s)) => s.to_string(),
6428                    _ => return Err(runtime_err("mask_phone: need string")),
6429                };
6430                let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
6431                let masked = if digits.len() >= 4 {
6432                    let last4 = &digits[digits.len() - 4..];
6433                    format!("***-***-{last4}")
6434                } else {
6435                    "***".to_string()
6436                };
6437                Ok(VmValue::String(Arc::from(masked.as_str())))
6438            }
6439            BuiltinId::MaskCreditCard => {
6440                let cc = match args.first() {
6441                    Some(VmValue::String(s)) => s.to_string(),
6442                    _ => return Err(runtime_err("mask_cc: need string")),
6443                };
6444                let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
6445                let masked = if digits.len() >= 4 {
6446                    let last4 = &digits[digits.len() - 4..];
6447                    format!("****-****-****-{last4}")
6448                } else {
6449                    "****-****-****-****".to_string()
6450                };
6451                Ok(VmValue::String(Arc::from(masked.as_str())))
6452            }
6453            BuiltinId::Redact => {
6454                let val = match args.first() {
6455                    Some(v) => format!("{v}"),
6456                    _ => return Err(runtime_err("redact: need value")),
6457                };
6458                let policy = match args.get(1) {
6459                    Some(VmValue::String(s)) => s.to_string(),
6460                    _ => "full".to_string(),
6461                };
6462                let result = match policy.as_str() {
6463                    "full" => "***".to_string(),
6464                    "partial" => {
6465                        if val.len() > 2 {
6466                            format!("{}***{}", &val[..1], &val[val.len() - 1..])
6467                        } else {
6468                            "***".to_string()
6469                        }
6470                    }
6471                    "hash" => {
6472                        use sha2::Digest;
6473                        let hash = sha2::Sha256::digest(val.as_bytes());
6474                        format!("{:x}", hash)
6475                    }
6476                    _ => "***".to_string(),
6477                };
6478                Ok(VmValue::String(Arc::from(result.as_str())))
6479            }
6480            BuiltinId::Hash => {
6481                let val = match args.first() {
6482                    Some(VmValue::String(s)) => s.to_string(),
6483                    _ => return Err(runtime_err("hash: need string")),
6484                };
6485                let algo = match args.get(1) {
6486                    Some(VmValue::String(s)) => s.to_string(),
6487                    _ => "sha256".to_string(),
6488                };
6489                let result = match algo.as_str() {
6490                    "sha256" => {
6491                        use sha2::Digest;
6492                        format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6493                    }
6494                    "sha512" => {
6495                        use sha2::Digest;
6496                        format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6497                    }
6498                    "md5" => {
6499                        use md5::Digest;
6500                        format!("{:x}", md5::Md5::digest(val.as_bytes()))
6501                    }
6502                    _ => {
6503                        return Err(runtime_err(format!(
6504                            "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6505                        )));
6506                    }
6507                };
6508                Ok(VmValue::String(Arc::from(result.as_str())))
6509            }
6510
6511            // ── Phase 25: Async builtins (tokio-backed when async-runtime feature enabled) ──
6512            #[cfg(feature = "async-runtime")]
6513            BuiltinId::AsyncReadFile => {
6514                let rt = self.ensure_runtime();
6515                crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6516            }
6517            #[cfg(feature = "async-runtime")]
6518            BuiltinId::AsyncWriteFile => {
6519                let rt = self.ensure_runtime();
6520                crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6521            }
6522            #[cfg(feature = "async-runtime")]
6523            BuiltinId::AsyncHttpGet => {
6524                let rt = self.ensure_runtime();
6525                crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6526            }
6527            #[cfg(feature = "async-runtime")]
6528            BuiltinId::AsyncHttpPost => {
6529                let rt = self.ensure_runtime();
6530                crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6531            }
6532            #[cfg(feature = "async-runtime")]
6533            BuiltinId::AsyncSleep => {
6534                let rt = self.ensure_runtime();
6535                crate::async_runtime::async_sleep_impl(&rt, &args)
6536            }
6537            #[cfg(feature = "async-runtime")]
6538            BuiltinId::Select => crate::async_runtime::select_impl(&args),
6539            #[cfg(feature = "async-runtime")]
6540            BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6541            #[cfg(feature = "async-runtime")]
6542            BuiltinId::AsyncMap => {
6543                let rt = self.ensure_runtime();
6544                let stack_snapshot = self.stack.clone();
6545                crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6546            }
6547            #[cfg(feature = "async-runtime")]
6548            BuiltinId::AsyncFilter => {
6549                let rt = self.ensure_runtime();
6550                let stack_snapshot = self.stack.clone();
6551                crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6552            }
6553
6554            #[cfg(not(feature = "async-runtime"))]
6555            BuiltinId::AsyncReadFile
6556            | BuiltinId::AsyncWriteFile
6557            | BuiltinId::AsyncHttpGet
6558            | BuiltinId::AsyncHttpPost
6559            | BuiltinId::AsyncSleep
6560            | BuiltinId::Select
6561            | BuiltinId::AsyncMap
6562            | BuiltinId::AsyncFilter
6563            | BuiltinId::RaceAll => Err(runtime_err(format!(
6564                "{}: async builtins require the 'async-runtime' feature",
6565                builtin_id.name()
6566            ))),
6567
6568            // Phase 27: Data Error Hierarchy builtins
6569            BuiltinId::IsError => {
6570                if args.is_empty() {
6571                    return Err(runtime_err("is_error() expects 1 argument"));
6572                }
6573                let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6574                    &*e.type_name == "DataError" ||
6575                    &*e.type_name == "NetworkError" ||
6576                    &*e.type_name == "ConnectorError"
6577                );
6578                Ok(VmValue::Bool(is_err))
6579            }
6580            BuiltinId::ErrorType => {
6581                if args.is_empty() {
6582                    return Err(runtime_err("error_type() expects 1 argument"));
6583                }
6584                match &args[0] {
6585                    VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6586                    _ => Ok(VmValue::None),
6587                }
6588            }
6589
6590            // Phase 32: GPU Tensor Support
6591            #[cfg(feature = "gpu")]
6592            BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6593            #[cfg(not(feature = "gpu"))]
6594            BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6595
6596            #[cfg(feature = "gpu")]
6597            BuiltinId::ToGpu => {
6598                if args.is_empty() {
6599                    return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6600                }
6601                let gt = self.ensure_gpu_tensor(&args[0])?;
6602                Ok(VmValue::GpuTensor(gt))
6603            }
6604            #[cfg(not(feature = "gpu"))]
6605            BuiltinId::ToGpu => Err(runtime_err(
6606                "GPU operations not available. Build with --features gpu",
6607            )),
6608
6609            #[cfg(feature = "gpu")]
6610            BuiltinId::ToCpu => {
6611                if args.is_empty() {
6612                    return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6613                }
6614                match &args[0] {
6615                    VmValue::GpuTensor(gt) => {
6616                        let cpu = gt.to_cpu().map_err(runtime_err)?;
6617                        Ok(VmValue::Tensor(Arc::new(cpu)))
6618                    }
6619                    _ => Err(runtime_err(format!(
6620                        "to_cpu() expects a gpu_tensor, got {}",
6621                        args[0].type_name()
6622                    ))),
6623                }
6624            }
6625            #[cfg(not(feature = "gpu"))]
6626            BuiltinId::ToCpu => Err(runtime_err(
6627                "GPU operations not available. Build with --features gpu",
6628            )),
6629
6630            #[cfg(feature = "gpu")]
6631            BuiltinId::GpuMatmul => {
6632                if args.len() < 2 {
6633                    return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6634                }
6635                let a = self.ensure_gpu_tensor(&args[0])?;
6636                let b = self.ensure_gpu_tensor(&args[1])?;
6637                let ops = self.get_gpu_ops()?;
6638                let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6639                Ok(VmValue::GpuTensor(Arc::new(result)))
6640            }
6641            #[cfg(not(feature = "gpu"))]
6642            BuiltinId::GpuMatmul => Err(runtime_err(
6643                "GPU operations not available. Build with --features gpu",
6644            )),
6645
6646            #[cfg(feature = "gpu")]
6647            BuiltinId::GpuBatchPredict => {
6648                if args.len() < 2 {
6649                    return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6650                }
6651                match (&args[0], &args[1]) {
6652                    (VmValue::Model(model), VmValue::Tensor(input)) => {
6653                        let batch_size = args.get(2).and_then(|v| match v {
6654                            VmValue::Int(n) => Some(*n as usize),
6655                            _ => None,
6656                        });
6657                        let result =
6658                            tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6659                                .map_err(runtime_err)?;
6660                        Ok(VmValue::Tensor(Arc::new(result)))
6661                    }
6662                    _ => Err(runtime_err(
6663                        "gpu_batch_predict() expects (model, tensor, [batch_size])",
6664                    )),
6665                }
6666            }
6667            #[cfg(not(feature = "gpu"))]
6668            BuiltinId::GpuBatchPredict => Err(runtime_err(
6669                "GPU operations not available. Build with --features gpu",
6670            )),
6671            // Phase 34: AI Agent Framework
6672            #[cfg(feature = "native")]
6673            BuiltinId::Embed => {
6674                if args.is_empty() {
6675                    return Err(runtime_err("embed() requires a text argument"));
6676                }
6677                let text = match &args[0] {
6678                    VmValue::String(s) => s.to_string(),
6679                    _ => return Err(runtime_err("embed() expects a string")),
6680                };
6681                let model = args
6682                    .get(1)
6683                    .and_then(|v| match v {
6684                        VmValue::String(s) => Some(s.to_string()),
6685                        _ => None,
6686                    })
6687                    .unwrap_or_else(|| "text-embedding-3-small".to_string());
6688                let api_key = args
6689                    .get(2)
6690                    .and_then(|v| match v {
6691                        VmValue::String(s) => Some(s.to_string()),
6692                        _ => None,
6693                    })
6694                    .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6695                    .ok_or_else(|| {
6696                        runtime_err(
6697                            "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6698                        )
6699                    })?;
6700                let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6701                    .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6702                Ok(VmValue::Tensor(Arc::new(tensor)))
6703            }
6704            #[cfg(not(feature = "native"))]
6705            BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6706            #[cfg(feature = "native")]
6707            BuiltinId::HttpRequest => {
6708                self.check_permission("network")?;
6709                if args.len() < 2 {
6710                    return Err(runtime_err(
6711                        "http_request(method, url, headers?, body?) expects at least 2 args",
6712                    ));
6713                }
6714                let method = match &args[0] {
6715                    VmValue::String(s) => s.to_string(),
6716                    _ => return Err(runtime_err("http_request() method must be a string")),
6717                };
6718                let url = match &args[1] {
6719                    VmValue::String(s) => s.to_string(),
6720                    _ => return Err(runtime_err("http_request() url must be a string")),
6721                };
6722                let client = reqwest::blocking::Client::new();
6723                let mut builder = match method.to_uppercase().as_str() {
6724                    "GET" => client.get(&url),
6725                    "POST" => client.post(&url),
6726                    "PUT" => client.put(&url),
6727                    "DELETE" => client.delete(&url),
6728                    "PATCH" => client.patch(&url),
6729                    "HEAD" => client.head(&url),
6730                    _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6731                };
6732                // Set headers if provided
6733                if let Some(VmValue::Map(headers)) = args.get(2) {
6734                    for (key, val) in headers.iter() {
6735                        if let VmValue::String(v) = val {
6736                            builder = builder.header(key.as_ref(), v.as_ref());
6737                        }
6738                    }
6739                }
6740                // Set body if provided
6741                if let Some(VmValue::String(body)) = args.get(3) {
6742                    builder = builder.body(body.as_ref().to_string());
6743                }
6744                let resp = builder
6745                    .send()
6746                    .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6747                let status = resp.status().as_u16() as i64;
6748                let body = resp
6749                    .text()
6750                    .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6751                Ok(VmValue::Map(Box::new(vec![
6752                    (Arc::from("status"), VmValue::Int(status)),
6753                    (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6754                ])))
6755            }
6756            #[cfg(not(feature = "native"))]
6757            BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6758            #[cfg(feature = "native")]
6759            BuiltinId::RunAgent => {
6760                self.check_permission("network")?;
6761                if args.len() < 2 {
6762                    return Err(runtime_err(
6763                        "run_agent(agent, message, [history]) expects at least 2 arguments",
6764                    ));
6765                }
6766                let agent_def = match &args[0] {
6767                    VmValue::AgentDef(def) => def.clone(),
6768                    _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6769                };
6770                let message = match &args[1] {
6771                    VmValue::String(s) => s.to_string(),
6772                    _ => return Err(runtime_err("run_agent() second arg must be a string")),
6773                };
6774                // Optional 3rd arg: conversation history as list of [role, content] pairs
6775                let history = if args.len() >= 3 {
6776                    match &args[2] {
6777                        VmValue::List(items) => {
6778                            let mut hist = Vec::new();
6779                            for item in items.iter() {
6780                                if let VmValue::List(pair) = item
6781                                    && pair.len() >= 2
6782                                {
6783                                    let role = match &pair[0] {
6784                                        VmValue::String(s) => s.to_string(),
6785                                        _ => continue,
6786                                    };
6787                                    let content = match &pair[1] {
6788                                        VmValue::String(s) => s.to_string(),
6789                                        _ => continue,
6790                                    };
6791                                    hist.push((role, content));
6792                                }
6793                            }
6794                            Some(hist)
6795                        }
6796                        _ => None,
6797                    }
6798                } else {
6799                    None
6800                };
6801                self.exec_agent_loop(&agent_def, &message, history.as_deref())
6802            }
6803            #[cfg(not(feature = "native"))]
6804            BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6805
6806            // Phase G4: Streaming agent responses
6807            #[cfg(feature = "native")]
6808            BuiltinId::StreamAgent => {
6809                self.check_permission("network")?;
6810                if args.len() < 3 {
6811                    return Err(runtime_err(
6812                        "stream_agent(agent, message, callback) expects 3 arguments",
6813                    ));
6814                }
6815                let agent_def = match &args[0] {
6816                    VmValue::AgentDef(def) => def.clone(),
6817                    _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6818                };
6819                let message = match &args[1] {
6820                    VmValue::String(s) => s.to_string(),
6821                    _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6822                };
6823                let callback = args[2].clone();
6824
6825                let model = &agent_def.model;
6826                let system = agent_def.system_prompt.as_deref();
6827                let base_url = agent_def.base_url.as_deref();
6828                let api_key = agent_def.api_key.as_deref();
6829
6830                let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6831                let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6832                    .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6833
6834                let mut full_text = String::new();
6835                loop {
6836                    match reader.next_chunk() {
6837                        Ok(Some(chunk)) => {
6838                            full_text.push_str(&chunk);
6839                            let chunk_val = VmValue::String(Arc::from(&*chunk));
6840                            let _ = self.call_value(callback.clone(), &[chunk_val]);
6841                        }
6842                        Ok(None) => break,
6843                        Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6844                    }
6845                }
6846
6847                Ok(VmValue::String(Arc::from(&*full_text)))
6848            }
6849            #[cfg(not(feature = "native"))]
6850            BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6851
6852            // Phase E5: Random & Sampling
6853            #[cfg(feature = "native")]
6854            BuiltinId::Random => {
6855                let mut rng = rand::thread_rng();
6856                let val: f64 = rand::Rng::r#gen(&mut rng);
6857                Ok(VmValue::Float(val))
6858            }
6859            #[cfg(not(feature = "native"))]
6860            BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6861            #[cfg(feature = "native")]
6862            BuiltinId::RandomInt => {
6863                if args.len() < 2 {
6864                    return Err(runtime_err("random_int() expects min and max"));
6865                }
6866                let a = match &args[0] {
6867                    VmValue::Int(n) => *n,
6868                    _ => return Err(runtime_err("random_int() expects integers")),
6869                };
6870                let b = match &args[1] {
6871                    VmValue::Int(n) => *n,
6872                    _ => return Err(runtime_err("random_int() expects integers")),
6873                };
6874                if a >= b {
6875                    return Err(runtime_err("random_int() requires min < max"));
6876                }
6877                let mut rng = rand::thread_rng();
6878                let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6879                Ok(VmValue::Int(val))
6880            }
6881            #[cfg(not(feature = "native"))]
6882            BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6883            #[cfg(feature = "native")]
6884            BuiltinId::Sample => {
6885                use rand::seq::SliceRandom;
6886                if args.is_empty() {
6887                    return Err(runtime_err("sample() expects a list and count"));
6888                }
6889                let items = match &args[0] {
6890                    VmValue::List(items) => items,
6891                    _ => return Err(runtime_err("sample() expects a list")),
6892                };
6893                let k = match args.get(1) {
6894                    Some(VmValue::Int(n)) => *n as usize,
6895                    _ => 1,
6896                };
6897                if k > items.len() {
6898                    return Err(runtime_err("sample() count exceeds list length"));
6899                }
6900                let mut rng = rand::thread_rng();
6901                let mut indices: Vec<usize> = (0..items.len()).collect();
6902                indices.partial_shuffle(&mut rng, k);
6903                let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6904                if k == 1 && args.get(1).is_none() {
6905                    Ok(result.into_iter().next().unwrap_or(VmValue::None))
6906                } else {
6907                    Ok(VmValue::List(Box::new(result)))
6908                }
6909            }
6910            #[cfg(not(feature = "native"))]
6911            BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6912
6913            // Phase E6: Math builtins
6914            BuiltinId::Exp => {
6915                let x = match args.first() {
6916                    Some(VmValue::Float(f)) => *f,
6917                    Some(VmValue::Int(n)) => *n as f64,
6918                    _ => return Err(runtime_err("exp() expects a number")),
6919                };
6920                Ok(VmValue::Float(x.exp()))
6921            }
6922            BuiltinId::IsNan => {
6923                let result = match args.first() {
6924                    Some(VmValue::Float(f)) => f.is_nan(),
6925                    _ => false,
6926                };
6927                Ok(VmValue::Bool(result))
6928            }
6929            BuiltinId::IsInfinite => {
6930                let result = match args.first() {
6931                    Some(VmValue::Float(f)) => f.is_infinite(),
6932                    _ => false,
6933                };
6934                Ok(VmValue::Bool(result))
6935            }
6936            BuiltinId::Sign => match args.first() {
6937                Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6938                    1
6939                } else if *n < 0 {
6940                    -1
6941                } else {
6942                    0
6943                })),
6944                Some(VmValue::Float(f)) => {
6945                    if f.is_nan() {
6946                        Ok(VmValue::Float(f64::NAN))
6947                    } else if *f > 0.0 {
6948                        Ok(VmValue::Int(1))
6949                    } else if *f < 0.0 {
6950                        Ok(VmValue::Int(-1))
6951                    } else {
6952                        Ok(VmValue::Int(0))
6953                    }
6954                }
6955                _ => Err(runtime_err("sign() expects a number")),
6956            },
6957            // Phase E8: Table assertion
6958            #[cfg(feature = "native")]
6959            BuiltinId::AssertTableEq => {
6960                if args.len() < 2 {
6961                    return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6962                }
6963                let t1 = match &args[0] {
6964                    VmValue::Table(t) => t,
6965                    _ => {
6966                        return Err(runtime_err(
6967                            "assert_table_eq() first argument must be a table",
6968                        ));
6969                    }
6970                };
6971                let t2 = match &args[1] {
6972                    VmValue::Table(t) => t,
6973                    _ => {
6974                        return Err(runtime_err(
6975                            "assert_table_eq() second argument must be a table",
6976                        ));
6977                    }
6978                };
6979                // Compare schemas
6980                if t1.df.schema() != t2.df.schema() {
6981                    return Err(runtime_err(format!(
6982                        "assert_table_eq: schemas differ\n  left:  {:?}\n  right: {:?}",
6983                        t1.df.schema(),
6984                        t2.df.schema()
6985                    )));
6986                }
6987                // Collect both DataFrames
6988                let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6989                let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6990                // Flatten into rows and compare
6991                let rows1: Vec<String> = batches1
6992                    .iter()
6993                    .flat_map(|b| {
6994                        (0..b.num_rows()).map(move |r| {
6995                            (0..b.num_columns())
6996                                .map(|c| {
6997                                    let col = b.column(c);
6998                                    format!("{:?}", col.slice(r, 1))
6999                                })
7000                                .collect::<Vec<_>>()
7001                                .join(",")
7002                        })
7003                    })
7004                    .collect();
7005                let rows2: Vec<String> = batches2
7006                    .iter()
7007                    .flat_map(|b| {
7008                        (0..b.num_rows()).map(move |r| {
7009                            (0..b.num_columns())
7010                                .map(|c| {
7011                                    let col = b.column(c);
7012                                    format!("{:?}", col.slice(r, 1))
7013                                })
7014                                .collect::<Vec<_>>()
7015                                .join(",")
7016                        })
7017                    })
7018                    .collect();
7019                if rows1.len() != rows2.len() {
7020                    return Err(runtime_err(format!(
7021                        "assert_table_eq: row count differs ({} vs {})",
7022                        rows1.len(),
7023                        rows2.len()
7024                    )));
7025                }
7026                for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
7027                    if r1 != r2 {
7028                        return Err(runtime_err(format!(
7029                            "assert_table_eq: row {} differs\n  left:  {}\n  right: {}",
7030                            i, r1, r2
7031                        )));
7032                    }
7033                }
7034                Ok(VmValue::None)
7035            }
7036            #[cfg(not(feature = "native"))]
7037            BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
7038
7039            // Phase F1: Date/Time builtins
7040            BuiltinId::Today => {
7041                use chrono::{Datelike, TimeZone};
7042                let now = chrono::Utc::now();
7043                let midnight = chrono::Utc
7044                    .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
7045                    .single()
7046                    .ok_or_else(|| runtime_err("Failed to compute today"))?;
7047                Ok(VmValue::DateTime(midnight.timestamp_millis()))
7048            }
7049            BuiltinId::DateAdd => {
7050                if args.len() < 3 {
7051                    return Err(runtime_err("date_add() expects datetime, amount, unit"));
7052                }
7053                let ms = match &args[0] {
7054                    VmValue::DateTime(ms) => *ms,
7055                    VmValue::Int(ms) => *ms,
7056                    _ => return Err(runtime_err("date_add() first arg must be datetime")),
7057                };
7058                let amount = match &args[1] {
7059                    VmValue::Int(n) => *n,
7060                    _ => return Err(runtime_err("date_add() amount must be an integer")),
7061                };
7062                let unit = match &args[2] {
7063                    VmValue::String(s) => s.as_ref(),
7064                    _ => return Err(runtime_err("date_add() unit must be a string")),
7065                };
7066                let offset_ms = match unit {
7067                    "second" | "seconds" => amount * 1000,
7068                    "minute" | "minutes" => amount * 60 * 1000,
7069                    "hour" | "hours" => amount * 3600 * 1000,
7070                    "day" | "days" => amount * 86400 * 1000,
7071                    "week" | "weeks" => amount * 7 * 86400 * 1000,
7072                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
7073                };
7074                Ok(VmValue::DateTime(ms + offset_ms))
7075            }
7076            BuiltinId::DateDiff => {
7077                if args.len() < 3 {
7078                    return Err(runtime_err(
7079                        "date_diff() expects datetime1, datetime2, unit",
7080                    ));
7081                }
7082                let ms1 = match &args[0] {
7083                    VmValue::DateTime(ms) => *ms,
7084                    VmValue::Int(ms) => *ms,
7085                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
7086                };
7087                let ms2 = match &args[1] {
7088                    VmValue::DateTime(ms) => *ms,
7089                    VmValue::Int(ms) => *ms,
7090                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
7091                };
7092                let unit = match &args[2] {
7093                    VmValue::String(s) => s.as_ref(),
7094                    _ => return Err(runtime_err("date_diff() unit must be a string")),
7095                };
7096                let diff_ms = ms1 - ms2;
7097                let result = match unit {
7098                    "second" | "seconds" => diff_ms / 1000,
7099                    "minute" | "minutes" => diff_ms / (60 * 1000),
7100                    "hour" | "hours" => diff_ms / (3600 * 1000),
7101                    "day" | "days" => diff_ms / (86400 * 1000),
7102                    "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
7103                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
7104                };
7105                Ok(VmValue::Int(result))
7106            }
7107            BuiltinId::DateTrunc => {
7108                if args.len() < 2 {
7109                    return Err(runtime_err("date_trunc() expects datetime and unit"));
7110                }
7111                let ms = match &args[0] {
7112                    VmValue::DateTime(ms) => *ms,
7113                    VmValue::Int(ms) => *ms,
7114                    _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
7115                };
7116                let unit = match &args[1] {
7117                    VmValue::String(s) => s.as_ref(),
7118                    _ => return Err(runtime_err("date_trunc() unit must be a string")),
7119                };
7120                use chrono::{Datelike, TimeZone, Timelike};
7121                let secs = ms / 1000;
7122                let dt = chrono::Utc
7123                    .timestamp_opt(secs, 0)
7124                    .single()
7125                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
7126                let truncated = match unit {
7127                    "second" => chrono::Utc
7128                        .with_ymd_and_hms(
7129                            dt.year(),
7130                            dt.month(),
7131                            dt.day(),
7132                            dt.hour(),
7133                            dt.minute(),
7134                            dt.second(),
7135                        )
7136                        .single(),
7137                    "minute" => chrono::Utc
7138                        .with_ymd_and_hms(
7139                            dt.year(),
7140                            dt.month(),
7141                            dt.day(),
7142                            dt.hour(),
7143                            dt.minute(),
7144                            0,
7145                        )
7146                        .single(),
7147                    "hour" => chrono::Utc
7148                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
7149                        .single(),
7150                    "day" => chrono::Utc
7151                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
7152                        .single(),
7153                    "month" => chrono::Utc
7154                        .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
7155                        .single(),
7156                    "year" => chrono::Utc
7157                        .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
7158                        .single(),
7159                    _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
7160                };
7161                Ok(VmValue::DateTime(
7162                    truncated
7163                        .ok_or_else(|| runtime_err("Invalid truncation"))?
7164                        .timestamp_millis(),
7165                ))
7166            }
7167            BuiltinId::DateExtract => {
7168                if args.len() < 2 {
7169                    return Err(runtime_err("extract() expects datetime and part"));
7170                }
7171                let ms = match &args[0] {
7172                    VmValue::DateTime(ms) => *ms,
7173                    VmValue::Int(ms) => *ms,
7174                    _ => return Err(runtime_err("extract() first arg must be datetime")),
7175                };
7176                let part = match &args[1] {
7177                    VmValue::String(s) => s.as_ref(),
7178                    _ => return Err(runtime_err("extract() part must be a string")),
7179                };
7180                use chrono::{Datelike, TimeZone, Timelike};
7181                let secs = ms / 1000;
7182                let dt = chrono::Utc
7183                    .timestamp_opt(secs, 0)
7184                    .single()
7185                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
7186                let val = match part {
7187                    "year" => dt.year() as i64,
7188                    "month" => dt.month() as i64,
7189                    "day" => dt.day() as i64,
7190                    "hour" => dt.hour() as i64,
7191                    "minute" => dt.minute() as i64,
7192                    "second" => dt.second() as i64,
7193                    "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
7194                    "day_of_year" | "doy" => dt.ordinal() as i64,
7195                    _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
7196                };
7197                Ok(VmValue::Int(val))
7198            }
7199
7200            // ── MCP builtins ──
7201            #[cfg(feature = "mcp")]
7202            BuiltinId::McpConnect => {
7203                if args.is_empty() {
7204                    return Err(runtime_err(
7205                        "mcp_connect expects at least 1 argument: command or URL",
7206                    ));
7207                }
7208                let command = match &args[0] {
7209                    VmValue::String(s) => s.to_string(),
7210                    _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
7211                };
7212
7213                // Build sampling callback when tl-ai is available (native feature)
7214                #[cfg(feature = "native")]
7215                let sampling_cb: Option<tl_mcp::SamplingCallback> =
7216                    Some(Arc::new(|req: tl_mcp::SamplingRequest| {
7217                        let model = req
7218                            .model_hint
7219                            .as_deref()
7220                            .unwrap_or("claude-sonnet-4-20250514");
7221                        let messages: Vec<serde_json::Value> = req
7222                            .messages
7223                            .iter()
7224                            .map(|(role, content)| {
7225                                serde_json::json!({"role": role, "content": content})
7226                            })
7227                            .collect();
7228                        let response = tl_ai::chat_with_tools(
7229                            model,
7230                            req.system_prompt.as_deref(),
7231                            &messages,
7232                            &[],  // no tools for sampling
7233                            None, // base_url
7234                            None, // api_key
7235                            None, // output_format
7236                        )
7237                        .map_err(|e| format!("Sampling LLM error: {e}"))?;
7238                        match response {
7239                            tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
7240                                model: model.to_string(),
7241                                content: text,
7242                                stop_reason: Some("endTurn".to_string()),
7243                            }),
7244                            tl_ai::LlmResponse::ToolUse(_) => {
7245                                Err("Sampling does not support tool use".to_string())
7246                            }
7247                        }
7248                    }));
7249
7250                #[cfg(not(feature = "native"))]
7251                let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
7252
7253                // Auto-detect HTTP URL vs subprocess command
7254                let client = if command.starts_with("http://") || command.starts_with("https://") {
7255                    tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
7256                        .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
7257                } else {
7258                    let cmd_args: Vec<String> = args[1..]
7259                        .iter()
7260                        .map(|a| match a {
7261                            VmValue::String(s) => s.to_string(),
7262                            other => format!("{}", other),
7263                        })
7264                        .collect();
7265                    tl_mcp::McpClient::connect_with_sampling(
7266                        &command,
7267                        &cmd_args,
7268                        self.security_policy.as_ref(),
7269                        sampling_cb,
7270                    )
7271                    .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
7272                };
7273                Ok(VmValue::McpClient(Arc::new(client)))
7274            }
7275            #[cfg(not(feature = "mcp"))]
7276            BuiltinId::McpConnect => {
7277                Err(runtime_err("MCP not available. Build with --features mcp"))
7278            }
7279
7280            #[cfg(feature = "mcp")]
7281            BuiltinId::McpListTools => {
7282                if args.is_empty() {
7283                    return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
7284                }
7285                match &args[0] {
7286                    VmValue::McpClient(client) => {
7287                        let tools = client
7288                            .list_tools()
7289                            .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
7290                        let tool_values: Vec<VmValue> = tools
7291                            .iter()
7292                            .map(|tool| {
7293                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7294                                pairs.push((
7295                                    Arc::from("name"),
7296                                    VmValue::String(Arc::from(tool.name.as_ref())),
7297                                ));
7298                                if let Some(desc) = &tool.description {
7299                                    pairs.push((
7300                                        Arc::from("description"),
7301                                        VmValue::String(Arc::from(desc.as_ref())),
7302                                    ));
7303                                }
7304                                let schema_json = serde_json::to_string(tool.input_schema.as_ref())
7305                                    .unwrap_or_default();
7306                                if !schema_json.is_empty() && schema_json != "{}" {
7307                                    pairs.push((
7308                                        Arc::from("input_schema"),
7309                                        VmValue::String(Arc::from(schema_json.as_str())),
7310                                    ));
7311                                }
7312                                VmValue::Map(Box::new(pairs))
7313                            })
7314                            .collect();
7315                        Ok(VmValue::List(Box::new(tool_values)))
7316                    }
7317                    _ => Err(runtime_err(
7318                        "mcp_list_tools: argument must be an mcp_client",
7319                    )),
7320                }
7321            }
7322            #[cfg(not(feature = "mcp"))]
7323            BuiltinId::McpListTools => {
7324                Err(runtime_err("MCP not available. Build with --features mcp"))
7325            }
7326
7327            #[cfg(feature = "mcp")]
7328            BuiltinId::McpCallTool => {
7329                if args.len() < 2 {
7330                    return Err(runtime_err(
7331                        "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
7332                    ));
7333                }
7334                let client = match &args[0] {
7335                    VmValue::McpClient(c) => c.clone(),
7336                    _ => {
7337                        return Err(runtime_err(
7338                            "mcp_call_tool: first argument must be an mcp_client",
7339                        ));
7340                    }
7341                };
7342                let tool_name = match &args[1] {
7343                    VmValue::String(s) => s.to_string(),
7344                    _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
7345                };
7346                let arguments = if args.len() > 2 {
7347                    vm_value_to_json(&args[2])
7348                } else {
7349                    serde_json::Value::Object(serde_json::Map::new())
7350                };
7351                let result = client
7352                    .call_tool(&tool_name, arguments)
7353                    .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
7354                let mut content_parts: Vec<VmValue> = Vec::new();
7355                for content in &result.content {
7356                    if let Some(text) = content.as_text() {
7357                        content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
7358                    }
7359                }
7360                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7361                if content_parts.len() == 1 {
7362                    pairs.push((
7363                        Arc::from("content"),
7364                        content_parts.into_iter().next().unwrap(),
7365                    ));
7366                } else {
7367                    pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
7368                }
7369                pairs.push((
7370                    Arc::from("is_error"),
7371                    VmValue::Bool(result.is_error.unwrap_or(false)),
7372                ));
7373                Ok(VmValue::Map(Box::new(pairs)))
7374            }
7375            #[cfg(not(feature = "mcp"))]
7376            BuiltinId::McpCallTool => {
7377                Err(runtime_err("MCP not available. Build with --features mcp"))
7378            }
7379
7380            #[cfg(feature = "mcp")]
7381            BuiltinId::McpDisconnect => {
7382                if args.is_empty() {
7383                    return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
7384                }
7385                match &args[0] {
7386                    VmValue::McpClient(_) => Ok(VmValue::None),
7387                    _ => Err(runtime_err(
7388                        "mcp_disconnect: argument must be an mcp_client",
7389                    )),
7390                }
7391            }
7392            #[cfg(not(feature = "mcp"))]
7393            BuiltinId::McpDisconnect => {
7394                Err(runtime_err("MCP not available. Build with --features mcp"))
7395            }
7396
7397            #[cfg(feature = "mcp")]
7398            BuiltinId::McpPing => {
7399                if args.is_empty() {
7400                    return Err(runtime_err("mcp_ping expects 1 argument: client"));
7401                }
7402                match &args[0] {
7403                    VmValue::McpClient(client) => {
7404                        client
7405                            .ping()
7406                            .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
7407                        Ok(VmValue::Bool(true))
7408                    }
7409                    _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
7410                }
7411            }
7412            #[cfg(not(feature = "mcp"))]
7413            BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
7414
7415            #[cfg(feature = "mcp")]
7416            BuiltinId::McpServerInfo => {
7417                if args.is_empty() {
7418                    return Err(runtime_err("mcp_server_info expects 1 argument: client"));
7419                }
7420                match &args[0] {
7421                    VmValue::McpClient(client) => match client.server_info() {
7422                        Some(info) => {
7423                            let pairs: Vec<(Arc<str>, VmValue)> = vec![
7424                                (
7425                                    Arc::from("name"),
7426                                    VmValue::String(Arc::from(info.server_info.name.as_str())),
7427                                ),
7428                                (
7429                                    Arc::from("version"),
7430                                    VmValue::String(Arc::from(info.server_info.version.as_str())),
7431                                ),
7432                            ];
7433                            Ok(VmValue::Map(Box::new(pairs)))
7434                        }
7435                        None => Ok(VmValue::None),
7436                    },
7437                    _ => Err(runtime_err(
7438                        "mcp_server_info: argument must be an mcp_client",
7439                    )),
7440                }
7441            }
7442            #[cfg(not(feature = "mcp"))]
7443            BuiltinId::McpServerInfo => {
7444                Err(runtime_err("MCP not available. Build with --features mcp"))
7445            }
7446
7447            #[cfg(feature = "mcp")]
7448            BuiltinId::McpServe => {
7449                self.check_permission("network")?;
7450                if args.is_empty() {
7451                    return Err(runtime_err(
7452                        "mcp_serve expects 1 argument: list of tool definitions",
7453                    ));
7454                }
7455                let tool_list = match &args[0] {
7456                    VmValue::List(items) => items.as_ref().clone(),
7457                    _ => {
7458                        return Err(runtime_err(
7459                            "mcp_serve: argument must be a list of tool maps",
7460                        ));
7461                    }
7462                };
7463
7464                // Extract tool definitions and function values
7465                let mut channel_tools = Vec::new();
7466                let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
7467
7468                for item in &tool_list {
7469                    let pairs = match item {
7470                        VmValue::Map(p) => p.as_ref(),
7471                        _ => {
7472                            return Err(runtime_err(
7473                                "mcp_serve: each tool must be a map with name, description, handler",
7474                            ));
7475                        }
7476                    };
7477                    let mut name = String::new();
7478                    let mut description = String::new();
7479                    let mut handler = None;
7480                    let mut input_schema = serde_json::json!({"type": "object"});
7481
7482                    for (k, v) in pairs {
7483                        match k.as_ref() {
7484                            "name" => {
7485                                if let VmValue::String(s) = v {
7486                                    name = s.to_string();
7487                                }
7488                            }
7489                            "description" => {
7490                                if let VmValue::String(s) = v {
7491                                    description = s.to_string();
7492                                }
7493                            }
7494                            "handler" => {
7495                                handler = Some(v.clone());
7496                            }
7497                            "input_schema" | "parameters" => {
7498                                if let VmValue::String(s) = v
7499                                    && let Ok(parsed) =
7500                                        serde_json::from_str::<serde_json::Value>(s.as_ref())
7501                                {
7502                                    input_schema = parsed;
7503                                }
7504                            }
7505                            _ => {}
7506                        }
7507                    }
7508
7509                    if name.is_empty() {
7510                        return Err(runtime_err("mcp_serve: tool missing 'name'"));
7511                    }
7512                    if let Some(h) = handler {
7513                        tool_handlers.insert(name.clone(), h);
7514                    }
7515
7516                    channel_tools.push(tl_mcp::server::ChannelToolDef {
7517                        name,
7518                        description,
7519                        input_schema,
7520                    });
7521                }
7522
7523                // Build server with channel-based tools
7524                let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7525                    .name("tl-mcp-server")
7526                    .version("1.0.0")
7527                    .channel_tools(channel_tools);
7528                let server_handler = builder.build();
7529
7530                // Start server on background thread
7531                let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7532
7533                // Main dispatch loop: process tool call requests from the MCP server
7534                while let Ok(req) = rx.recv() {
7535                    let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7536                        // Convert JSON args to VmValue args
7537                        let call_args = self.json_to_vm_args(&req.arguments);
7538                        match self.call_value(func.clone(), &call_args) {
7539                            Ok(val) => {
7540                                // Convert VmValue to JSON-friendly string
7541                                Ok(serde_json::json!(format!("{val}")))
7542                            }
7543                            Err(e) => Err(format!("{e}")),
7544                        }
7545                    } else {
7546                        Err(format!("Unknown tool: {}", req.tool_name))
7547                    };
7548                    let _ = req.response_tx.send(result);
7549                }
7550
7551                Ok(VmValue::None)
7552            }
7553            #[cfg(not(feature = "mcp"))]
7554            BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7555
7556            // ── MCP Resources & Prompts ──
7557            #[cfg(feature = "mcp")]
7558            BuiltinId::McpListResources => {
7559                if args.is_empty() {
7560                    return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7561                }
7562                match &args[0] {
7563                    VmValue::McpClient(client) => {
7564                        let resources = client
7565                            .list_resources()
7566                            .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7567                        let vals: Vec<VmValue> = resources
7568                            .iter()
7569                            .map(|r| {
7570                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7571                                pairs.push((
7572                                    Arc::from("uri"),
7573                                    VmValue::String(Arc::from(r.uri.as_str())),
7574                                ));
7575                                pairs.push((
7576                                    Arc::from("name"),
7577                                    VmValue::String(Arc::from(r.name.as_str())),
7578                                ));
7579                                if let Some(desc) = &r.description {
7580                                    pairs.push((
7581                                        Arc::from("description"),
7582                                        VmValue::String(Arc::from(desc.as_str())),
7583                                    ));
7584                                }
7585                                if let Some(mime) = &r.mime_type {
7586                                    pairs.push((
7587                                        Arc::from("mime_type"),
7588                                        VmValue::String(Arc::from(mime.as_str())),
7589                                    ));
7590                                }
7591                                VmValue::Map(Box::new(pairs))
7592                            })
7593                            .collect();
7594                        Ok(VmValue::List(Box::new(vals)))
7595                    }
7596                    _ => Err(runtime_err(
7597                        "mcp_list_resources: argument must be an mcp_client",
7598                    )),
7599                }
7600            }
7601            #[cfg(not(feature = "mcp"))]
7602            BuiltinId::McpListResources => {
7603                Err(runtime_err("MCP not available. Build with --features mcp"))
7604            }
7605
7606            #[cfg(feature = "mcp")]
7607            BuiltinId::McpReadResource => {
7608                if args.len() < 2 {
7609                    return Err(runtime_err(
7610                        "mcp_read_resource expects 2 arguments: client, uri",
7611                    ));
7612                }
7613                let client = match &args[0] {
7614                    VmValue::McpClient(c) => c.clone(),
7615                    _ => {
7616                        return Err(runtime_err(
7617                            "mcp_read_resource: first argument must be an mcp_client",
7618                        ));
7619                    }
7620                };
7621                let uri = match &args[1] {
7622                    VmValue::String(s) => s.to_string(),
7623                    _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7624                };
7625                let result = client
7626                    .read_resource(&uri)
7627                    .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7628                // Serialize ResourceContents via JSON to avoid direct rmcp type dependency
7629                let contents: Vec<VmValue> = result
7630                    .contents
7631                    .iter()
7632                    .map(|content| {
7633                        let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7634                        let json = serde_json::to_value(content).unwrap_or_default();
7635                        if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7636                            pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7637                        }
7638                        if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7639                            pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7640                        }
7641                        if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7642                            pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7643                        }
7644                        if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7645                            pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7646                        }
7647                        VmValue::Map(Box::new(pairs))
7648                    })
7649                    .collect();
7650                if contents.len() == 1 {
7651                    Ok(contents.into_iter().next().unwrap())
7652                } else {
7653                    Ok(VmValue::List(Box::new(contents)))
7654                }
7655            }
7656            #[cfg(not(feature = "mcp"))]
7657            BuiltinId::McpReadResource => {
7658                Err(runtime_err("MCP not available. Build with --features mcp"))
7659            }
7660
7661            #[cfg(feature = "mcp")]
7662            BuiltinId::McpListPrompts => {
7663                if args.is_empty() {
7664                    return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7665                }
7666                match &args[0] {
7667                    VmValue::McpClient(client) => {
7668                        let prompts = client
7669                            .list_prompts()
7670                            .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7671                        let vals: Vec<VmValue> = prompts
7672                            .iter()
7673                            .map(|p| {
7674                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7675                                pairs.push((
7676                                    Arc::from("name"),
7677                                    VmValue::String(Arc::from(p.name.as_str())),
7678                                ));
7679                                if let Some(desc) = &p.description {
7680                                    pairs.push((
7681                                        Arc::from("description"),
7682                                        VmValue::String(Arc::from(desc.as_str())),
7683                                    ));
7684                                }
7685                                if let Some(prompt_args) = &p.arguments {
7686                                    let arg_vals: Vec<VmValue> = prompt_args
7687                                        .iter()
7688                                        .map(|a| {
7689                                            let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7690                                                Vec::new();
7691                                            arg_pairs.push((
7692                                                Arc::from("name"),
7693                                                VmValue::String(Arc::from(a.name.as_str())),
7694                                            ));
7695                                            if let Some(desc) = &a.description {
7696                                                arg_pairs.push((
7697                                                    Arc::from("description"),
7698                                                    VmValue::String(Arc::from(desc.as_str())),
7699                                                ));
7700                                            }
7701                                            arg_pairs.push((
7702                                                Arc::from("required"),
7703                                                VmValue::Bool(a.required.unwrap_or(false)),
7704                                            ));
7705                                            VmValue::Map(Box::new(arg_pairs))
7706                                        })
7707                                        .collect();
7708                                    pairs.push((
7709                                        Arc::from("arguments"),
7710                                        VmValue::List(Box::new(arg_vals)),
7711                                    ));
7712                                }
7713                                VmValue::Map(Box::new(pairs))
7714                            })
7715                            .collect();
7716                        Ok(VmValue::List(Box::new(vals)))
7717                    }
7718                    _ => Err(runtime_err(
7719                        "mcp_list_prompts: argument must be an mcp_client",
7720                    )),
7721                }
7722            }
7723            #[cfg(not(feature = "mcp"))]
7724            BuiltinId::McpListPrompts => {
7725                Err(runtime_err("MCP not available. Build with --features mcp"))
7726            }
7727
7728            #[cfg(feature = "mcp")]
7729            BuiltinId::McpGetPrompt => {
7730                if args.len() < 2 {
7731                    return Err(runtime_err(
7732                        "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7733                    ));
7734                }
7735                let client = match &args[0] {
7736                    VmValue::McpClient(c) => c.clone(),
7737                    _ => {
7738                        return Err(runtime_err(
7739                            "mcp_get_prompt: first argument must be an mcp_client",
7740                        ));
7741                    }
7742                };
7743                let name = match &args[1] {
7744                    VmValue::String(s) => s.to_string(),
7745                    _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7746                };
7747                let prompt_args = if args.len() > 2 {
7748                    let json = vm_value_to_json(&args[2]);
7749                    json.as_object().cloned()
7750                } else {
7751                    None
7752                };
7753                let result = client
7754                    .get_prompt(&name, prompt_args)
7755                    .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7756                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7757                if let Some(desc) = &result.description {
7758                    pairs.push((
7759                        Arc::from("description"),
7760                        VmValue::String(Arc::from(desc.as_str())),
7761                    ));
7762                }
7763                // Serialize PromptMessage via JSON to avoid direct rmcp type dependency
7764                let messages: Vec<VmValue> = result
7765                    .messages
7766                    .iter()
7767                    .map(|m| {
7768                        let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7769                        let msg_json = serde_json::to_value(m).unwrap_or_default();
7770                        // role is serialized as "user" or "assistant"
7771                        if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7772                            msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7773                        }
7774                        // content is an object with "type" field; extract text if it's a text message
7775                        if let Some(content) = msg_json.get("content") {
7776                            if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7777                                msg_pairs
7778                                    .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7779                            } else {
7780                                let content_str = content.to_string();
7781                                msg_pairs.push((
7782                                    Arc::from("content"),
7783                                    VmValue::String(Arc::from(content_str.as_str())),
7784                                ));
7785                            }
7786                        }
7787                        VmValue::Map(Box::new(msg_pairs))
7788                    })
7789                    .collect();
7790                pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7791                Ok(VmValue::Map(Box::new(pairs)))
7792            }
7793            #[cfg(not(feature = "mcp"))]
7794            BuiltinId::McpGetPrompt => {
7795                Err(runtime_err("MCP not available. Build with --features mcp"))
7796            }
7797        }
7798    }
7799
7800    // ── AI helpers ──
7801
7802    fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7803        match val {
7804            VmValue::List(items) => items
7805                .iter()
7806                .map(|item| match item {
7807                    VmValue::Int(n) => Ok(*n as f64),
7808                    VmValue::Float(f) => Ok(*f),
7809                    _ => Err(runtime_err("Expected number in list")),
7810                })
7811                .collect(),
7812            VmValue::Int(n) => Ok(vec![*n as f64]),
7813            VmValue::Float(f) => Ok(vec![*f]),
7814            _ => Err(runtime_err("Expected a list of numbers")),
7815        }
7816    }
7817
7818    fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7819        match val {
7820            VmValue::List(items) => items
7821                .iter()
7822                .map(|item| match item {
7823                    VmValue::Int(n) => Ok(*n as usize),
7824                    _ => Err(runtime_err("Expected integer in shape list")),
7825                })
7826                .collect(),
7827            _ => Err(runtime_err("Expected a list of integers for shape")),
7828        }
7829    }
7830
7831    #[cfg(feature = "native")]
7832    fn handle_train(
7833        &mut self,
7834        frame_idx: usize,
7835        algo_const: u8,
7836        config_const: u8,
7837    ) -> Result<VmValue, TlError> {
7838        let frame = &self.frames[frame_idx];
7839        let algorithm = match &frame.prototype.constants[algo_const as usize] {
7840            Constant::String(s) => s.to_string(),
7841            _ => return Err(runtime_err("Expected string constant for algorithm")),
7842        };
7843        let config_args = match &frame.prototype.constants[config_const as usize] {
7844            Constant::AstExprList(args) => args.clone(),
7845            _ => return Err(runtime_err("Expected AST expr list for train config")),
7846        };
7847
7848        // Extract config values
7849        let mut data_val = None;
7850        let mut target_name = None;
7851        let mut feature_names: Vec<String> = Vec::new();
7852
7853        for arg in &config_args {
7854            if let AstExpr::NamedArg { name, value } = arg {
7855                match name.as_str() {
7856                    "data" => {
7857                        data_val = Some(self.eval_ast_to_vm(value)?);
7858                    }
7859                    "target" => {
7860                        if let AstExpr::String(s) = value.as_ref() {
7861                            target_name = Some(s.clone());
7862                        }
7863                    }
7864                    "features" => {
7865                        if let AstExpr::List(items) = value.as_ref() {
7866                            for item in items {
7867                                if let AstExpr::String(s) = item {
7868                                    feature_names.push(s.clone());
7869                                }
7870                            }
7871                        }
7872                    }
7873                    _ => {}
7874                }
7875            }
7876        }
7877
7878        // Build training config from table data
7879        let table = match data_val {
7880            Some(VmValue::Table(t)) => t,
7881            _ => return Err(runtime_err("train: data must be a table")),
7882        };
7883        let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7884
7885        // Collect table to Arrow batches
7886        let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7887        if batches.is_empty() {
7888            return Err(runtime_err("train: empty dataset"));
7889        }
7890
7891        // Determine feature columns if not specified
7892        let batch = &batches[0];
7893        let schema = batch.schema();
7894        if feature_names.is_empty() {
7895            for field in schema.fields() {
7896                if field.name() != &target {
7897                    feature_names.push(field.name().clone());
7898                }
7899            }
7900        }
7901
7902        // Extract feature data and target data as f64 arrays
7903        let n_rows = batch.num_rows();
7904        let n_features = feature_names.len();
7905        let mut features_data = Vec::with_capacity(n_rows * n_features);
7906        let mut target_data = Vec::with_capacity(n_rows);
7907
7908        for col_name in &feature_names {
7909            let col_idx = schema
7910                .index_of(col_name)
7911                .map_err(|_| runtime_err(format!("Column not found: {col_name}")))?;
7912            let col_arr = batch.column(col_idx);
7913            Self::extract_f64_column(col_arr, &mut features_data)?;
7914        }
7915
7916        // Extract target column
7917        let target_idx = schema
7918            .index_of(&target)
7919            .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7920        let target_arr = batch.column(target_idx);
7921        Self::extract_f64_column(target_arr, &mut target_data)?;
7922
7923        // Reshape features: [col1_row1, col1_row2, ..., col2_row1, ...] → row-major
7924        let mut row_major = Vec::with_capacity(n_rows * n_features);
7925        for row in 0..n_rows {
7926            for col in 0..n_features {
7927                row_major.push(features_data[col * n_rows + row]);
7928            }
7929        }
7930
7931        let features_tensor = tl_ai::TlTensor::from_vec(row_major, &[n_rows, n_features])
7932            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7933        let target_tensor = tl_ai::TlTensor::from_vec(target_data, &[n_rows])
7934            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7935
7936        let config = tl_ai::TrainConfig {
7937            features: features_tensor,
7938            target: target_tensor,
7939            feature_names: feature_names.clone(),
7940            target_name: target.clone(),
7941            model_name: algorithm.clone(),
7942            split_ratio: 0.8,
7943            hyperparams: std::collections::HashMap::new(),
7944        };
7945
7946        let model = tl_ai::train(&algorithm, &config)
7947            .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7948
7949        Ok(VmValue::Model(Arc::new(model)))
7950    }
7951
7952    #[cfg(feature = "native")]
7953    fn extract_f64_column(
7954        col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7955        out: &mut Vec<f64>,
7956    ) -> Result<(), TlError> {
7957        use tl_data::datafusion::arrow::array::{
7958            Array, Float32Array, Float64Array, Int32Array, Int64Array,
7959        };
7960        let len = col.len();
7961        if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
7962            for i in 0..len {
7963                out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
7964            }
7965        } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
7966            for i in 0..len {
7967                out.push(if arr.is_null(i) {
7968                    0.0
7969                } else {
7970                    arr.value(i) as f64
7971                });
7972            }
7973        } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
7974            for i in 0..len {
7975                out.push(if arr.is_null(i) {
7976                    0.0
7977                } else {
7978                    arr.value(i) as f64
7979                });
7980            }
7981        } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
7982            for i in 0..len {
7983                out.push(if arr.is_null(i) {
7984                    0.0
7985                } else {
7986                    arr.value(i) as f64
7987                });
7988            }
7989        } else {
7990            return Err(runtime_err(
7991                "Column must be numeric (int32, int64, float32, float64)",
7992            ));
7993        }
7994        Ok(())
7995    }
7996
7997    #[cfg(feature = "native")]
7998    fn handle_pipeline_exec(
7999        &mut self,
8000        frame_idx: usize,
8001        name_const: u8,
8002        config_const: u8,
8003    ) -> Result<VmValue, TlError> {
8004        let frame = &self.frames[frame_idx];
8005        let name = match &frame.prototype.constants[name_const as usize] {
8006            Constant::String(s) => s.to_string(),
8007            _ => return Err(runtime_err("Expected string constant for pipeline name")),
8008        };
8009
8010        let mut schedule = None;
8011        let mut timeout_ms = None;
8012        let mut retries = 0u32;
8013
8014        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8015            for arg in args {
8016                if let AstExpr::NamedArg { name: key, value } = arg {
8017                    match key.as_str() {
8018                        "schedule" => {
8019                            if let AstExpr::String(s) = value.as_ref() {
8020                                schedule = Some(s.clone());
8021                            }
8022                        }
8023                        "timeout" => {
8024                            if let AstExpr::String(s) = value.as_ref() {
8025                                timeout_ms = tl_stream::parse_duration(s).ok();
8026                            }
8027                        }
8028                        "retries" => {
8029                            if let AstExpr::Int(n) = value.as_ref() {
8030                                retries = *n as u32;
8031                            }
8032                        }
8033                        _ => {}
8034                    }
8035                }
8036            }
8037        }
8038
8039        let def = tl_stream::PipelineDef {
8040            name,
8041            schedule,
8042            timeout_ms,
8043            retries,
8044        };
8045
8046        self.output
8047            .push(format!("Pipeline '{}': success", def.name));
8048        Ok(VmValue::PipelineDef(Arc::new(def)))
8049    }
8050
8051    #[cfg(feature = "native")]
8052    fn handle_stream_exec(
8053        &mut self,
8054        frame_idx: usize,
8055        config_const: u8,
8056    ) -> Result<VmValue, TlError> {
8057        let frame = &self.frames[frame_idx];
8058        let config_args = match &frame.prototype.constants[config_const as usize] {
8059            Constant::AstExprList(args) => args.clone(),
8060            _ => return Err(runtime_err("Expected AST expr list for stream config")),
8061        };
8062
8063        let mut name = String::new();
8064        let mut window = None;
8065        let mut watermark_ms = None;
8066
8067        for arg in &config_args {
8068            if let AstExpr::NamedArg { name: key, value } = arg {
8069                match key.as_str() {
8070                    "name" => {
8071                        if let AstExpr::String(s) = value.as_ref() {
8072                            name = s.clone();
8073                        }
8074                    }
8075                    "window" => {
8076                        if let AstExpr::String(s) = value.as_ref() {
8077                            window = Self::parse_window_type(s);
8078                        }
8079                    }
8080                    "watermark" => {
8081                        if let AstExpr::String(s) = value.as_ref() {
8082                            watermark_ms = tl_stream::parse_duration(s).ok();
8083                        }
8084                    }
8085                    _ => {}
8086                }
8087            }
8088        }
8089
8090        let def = tl_stream::StreamDef {
8091            name: name.clone(),
8092            window,
8093            watermark_ms,
8094        };
8095
8096        self.output.push(format!("Stream '{}' declared", name));
8097        Ok(VmValue::StreamDef(Arc::new(def)))
8098    }
8099
8100    #[cfg(feature = "native")]
8101    fn handle_agent_exec(
8102        &mut self,
8103        frame_idx: usize,
8104        name_const: u8,
8105        config_const: u8,
8106    ) -> Result<VmValue, TlError> {
8107        let frame = &self.frames[frame_idx];
8108        let name = match &frame.prototype.constants[name_const as usize] {
8109            Constant::String(s) => s.to_string(),
8110            _ => return Err(runtime_err("Expected string constant for agent name")),
8111        };
8112
8113        let mut model = String::new();
8114        let mut system_prompt = None;
8115        let mut max_turns = 10u32;
8116        let mut temperature = None;
8117        let mut max_tokens = None;
8118        let mut base_url = None;
8119        let mut api_key = None;
8120        let mut output_format = None;
8121        let mut tools = Vec::new();
8122        #[cfg(feature = "mcp")]
8123        let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
8124
8125        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8126            for arg in args {
8127                if let AstExpr::NamedArg { name: key, value } = arg {
8128                    if let Some(tool_name) = key.strip_prefix("tool:") {
8129                        // Tool definition — extract description and parameters from map expr
8130                        let (desc, params) = Self::extract_tool_from_ast(value);
8131                        tools.push(tl_stream::AgentTool {
8132                            name: tool_name.to_string(),
8133                            description: desc,
8134                            parameters: params,
8135                        });
8136                    } else if key.starts_with("mcp_server:") {
8137                        // MCP server reference — look up variable in globals
8138                        #[cfg(feature = "mcp")]
8139                        if let AstExpr::Ident(var_name) = value.as_ref()
8140                            && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
8141                        {
8142                            mcp_clients.push(client.clone());
8143                        }
8144                    } else {
8145                        match key.as_str() {
8146                            "model" => {
8147                                if let AstExpr::String(s) = value.as_ref() {
8148                                    model = s.clone();
8149                                }
8150                            }
8151                            "system" => {
8152                                if let AstExpr::String(s) = value.as_ref() {
8153                                    system_prompt = Some(s.clone());
8154                                }
8155                            }
8156                            "max_turns" => {
8157                                if let AstExpr::Int(n) = value.as_ref() {
8158                                    max_turns = *n as u32;
8159                                }
8160                            }
8161                            "temperature" => {
8162                                if let AstExpr::Float(f) = value.as_ref() {
8163                                    temperature = Some(*f);
8164                                }
8165                            }
8166                            "max_tokens" => {
8167                                if let AstExpr::Int(n) = value.as_ref() {
8168                                    max_tokens = Some(*n as u32);
8169                                }
8170                            }
8171                            "base_url" => {
8172                                if let AstExpr::String(s) = value.as_ref() {
8173                                    base_url = Some(s.clone());
8174                                }
8175                            }
8176                            "api_key" => {
8177                                if let AstExpr::String(s) = value.as_ref() {
8178                                    api_key = Some(s.clone());
8179                                }
8180                            }
8181                            "output_format" => {
8182                                if let AstExpr::String(s) = value.as_ref() {
8183                                    output_format = Some(s.clone());
8184                                }
8185                            }
8186                            _ => {}
8187                        }
8188                    }
8189                }
8190            }
8191        }
8192
8193        let def = tl_stream::AgentDef {
8194            name: name.clone(),
8195            model,
8196            system_prompt,
8197            tools,
8198            max_turns,
8199            temperature,
8200            max_tokens,
8201            base_url,
8202            api_key,
8203            output_format,
8204        };
8205
8206        // Store MCP clients for this agent
8207        #[cfg(feature = "mcp")]
8208        if !mcp_clients.is_empty() {
8209            self.mcp_agent_clients.insert(name.clone(), mcp_clients);
8210        }
8211
8212        Ok(VmValue::AgentDef(Arc::new(def)))
8213    }
8214
8215    #[cfg(feature = "native")]
8216    fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
8217        let mut desc = String::new();
8218        let mut params = serde_json::Value::Object(serde_json::Map::new());
8219        if let AstExpr::Map(pairs) = expr {
8220            for (key_expr, val_expr) in pairs {
8221                if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
8222                    match key.as_str() {
8223                        "description" => {
8224                            if let AstExpr::String(s) = val_expr {
8225                                desc = s.clone();
8226                            }
8227                        }
8228                        "parameters" => {
8229                            params = Self::ast_to_json(val_expr);
8230                        }
8231                        _ => {}
8232                    }
8233                }
8234            }
8235        }
8236        (desc, params)
8237    }
8238
8239    #[cfg(feature = "native")]
8240    fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
8241        match expr {
8242            AstExpr::String(s) => serde_json::Value::String(s.clone()),
8243            AstExpr::Int(n) => serde_json::json!(*n),
8244            AstExpr::Float(f) => serde_json::json!(*f),
8245            AstExpr::Bool(b) => serde_json::Value::Bool(*b),
8246            AstExpr::None => serde_json::Value::Null,
8247            AstExpr::List(items) => {
8248                serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
8249            }
8250            AstExpr::Map(pairs) => {
8251                let mut map = serde_json::Map::new();
8252                for (k, v) in pairs {
8253                    let key = match k {
8254                        AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
8255                        _ => format!("{k:?}"),
8256                    };
8257                    map.insert(key, Self::ast_to_json(v));
8258                }
8259                serde_json::Value::Object(map)
8260            }
8261            _ => serde_json::Value::Null,
8262        }
8263    }
8264
8265    #[cfg(feature = "native")]
8266    fn exec_agent_loop(
8267        &mut self,
8268        agent_def: &tl_stream::AgentDef,
8269        user_message: &str,
8270        history: Option<&[(String, String)]>,
8271    ) -> Result<VmValue, TlError> {
8272        use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
8273
8274        let model = &agent_def.model;
8275        let system = agent_def.system_prompt.as_deref();
8276        let base_url = agent_def.base_url.as_deref();
8277        let api_key = agent_def.api_key.as_deref();
8278
8279        let provider = if model.starts_with("claude") {
8280            "anthropic"
8281        } else {
8282            "openai"
8283        };
8284
8285        // Build tools JSON in OpenAI format from TL-declared tools
8286        #[allow(unused_mut)]
8287        let mut tools_json: Vec<serde_json::Value> = agent_def
8288            .tools
8289            .iter()
8290            .map(|t| {
8291                serde_json::json!({
8292                    "type": "function",
8293                    "function": {
8294                        "name": t.name,
8295                        "description": t.description,
8296                        "parameters": t.parameters
8297                    }
8298                })
8299            })
8300            .collect();
8301
8302        // Add MCP tools from connected servers
8303        #[cfg(feature = "mcp")]
8304        let mcp_clients = self
8305            .mcp_agent_clients
8306            .get(&agent_def.name)
8307            .cloned()
8308            .unwrap_or_default();
8309        #[cfg(feature = "mcp")]
8310        let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
8311            let mut dispatch = std::collections::HashMap::new();
8312            for (client_idx, client) in mcp_clients.iter().enumerate() {
8313                if let Ok(mcp_tools) = client.list_tools() {
8314                    for tool in mcp_tools {
8315                        let tool_name = tool.name.to_string();
8316                        tools_json.push(serde_json::json!({
8317                            "type": "function",
8318                            "function": {
8319                                "name": &tool_name,
8320                                "description": tool.description.as_deref().unwrap_or(""),
8321                                "parameters": serde_json::Value::Object((*tool.input_schema).clone())
8322                            }
8323                        }));
8324                        dispatch.insert(tool_name, client_idx);
8325                    }
8326                }
8327            }
8328            dispatch
8329        };
8330
8331        // Seed messages with history if provided
8332        let mut messages: Vec<serde_json::Value> = Vec::new();
8333        if let Some(hist) = history {
8334            for (role, content) in hist {
8335                messages.push(serde_json::json!({"role": role, "content": content}));
8336            }
8337        }
8338        // Add the current user message
8339        messages.push(serde_json::json!({
8340            "role": "user",
8341            "content": user_message
8342        }));
8343
8344        for turn in 0..agent_def.max_turns {
8345            let response = chat_with_tools(
8346                model,
8347                system,
8348                &messages,
8349                &tools_json,
8350                base_url,
8351                api_key,
8352                agent_def.output_format.as_deref(),
8353            )
8354            .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
8355
8356            match response {
8357                LlmResponse::Text(text) => {
8358                    // Add assistant response to history
8359                    messages.push(serde_json::json!({"role": "assistant", "content": &text}));
8360
8361                    // Build conversation history as list of [role, content] pairs
8362                    let history_list: Vec<VmValue> = messages
8363                        .iter()
8364                        .filter_map(|m| {
8365                            let role = m["role"].as_str()?;
8366                            let content = m["content"].as_str()?;
8367                            Some(VmValue::List(Box::new(vec![
8368                                VmValue::String(Arc::from(role)),
8369                                VmValue::String(Arc::from(content)),
8370                            ])))
8371                        })
8372                        .collect();
8373
8374                    // Agent completed — return result map with history
8375                    let result = VmValue::Map(Box::new(vec![
8376                        (
8377                            Arc::from("response"),
8378                            VmValue::String(Arc::from(text.as_str())),
8379                        ),
8380                        (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
8381                        (Arc::from("history"), VmValue::List(Box::new(history_list))),
8382                    ]));
8383
8384                    // Call on_complete lifecycle hook if defined
8385                    let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
8386                    if let Some(hook) = self.globals.get(&hook_name).cloned() {
8387                        let _ = self.call_value(hook, std::slice::from_ref(&result));
8388                    }
8389
8390                    return Ok(result);
8391                }
8392                LlmResponse::ToolUse(tool_calls) => {
8393                    // Add assistant message with tool calls for context
8394                    let tc_json: Vec<serde_json::Value> = tool_calls
8395                        .iter()
8396                        .map(|tc| {
8397                            serde_json::json!({
8398                                "id": tc.id,
8399                                "type": "function",
8400                                "function": {
8401                                    "name": tc.name,
8402                                    "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
8403                                }
8404                            })
8405                        })
8406                        .collect();
8407                    messages.push(serde_json::json!({
8408                        "role": "assistant",
8409                        "tool_calls": tc_json
8410                    }));
8411
8412                    // Build declared tool names (TL tools + MCP tools)
8413                    #[allow(unused_mut)]
8414                    let mut declared: Vec<String> =
8415                        agent_def.tools.iter().map(|t| t.name.clone()).collect();
8416                    #[cfg(feature = "mcp")]
8417                    {
8418                        for name in mcp_tool_dispatch.keys() {
8419                            declared.push(name.clone());
8420                        }
8421                    }
8422
8423                    // Execute each tool call
8424                    let mut results: Vec<(String, String)> = Vec::new();
8425                    for tc in &tool_calls {
8426                        if !declared.iter().any(|d| d == &tc.name) {
8427                            results.push((
8428                                tc.name.clone(),
8429                                format!("Error: '{}' not in declared tools", tc.name),
8430                            ));
8431                            continue;
8432                        }
8433
8434                        // Try MCP dispatch first, then fall back to TL function lookup
8435                        let result_str;
8436                        #[cfg(feature = "mcp")]
8437                        {
8438                            if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
8439                                let mcp_result = mcp_clients[client_idx]
8440                                    .call_tool(&tc.name, tc.input.clone())
8441                                    .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
8442                                result_str = mcp_result
8443                                    .content
8444                                    .iter()
8445                                    .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
8446                                    .collect::<Vec<_>>()
8447                                    .join("\n");
8448                            } else {
8449                                result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8450                            }
8451                        }
8452                        #[cfg(not(feature = "mcp"))]
8453                        {
8454                            result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8455                        }
8456
8457                        // Call on_tool_call lifecycle hook if defined
8458                        let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
8459                        if let Some(hook) = self.globals.get(&hook_name).cloned() {
8460                            let hook_args = vec![
8461                                VmValue::String(Arc::from(tc.name.as_str())),
8462                                self.json_value_to_vm(&tc.input),
8463                                VmValue::String(Arc::from(result_str.as_str())),
8464                            ];
8465                            let _ = self.call_value(hook, &hook_args);
8466                        }
8467
8468                        results.push((tc.name.clone(), result_str));
8469                    }
8470
8471                    // Format tool results and add to messages
8472                    let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
8473                    messages.extend(result_msgs);
8474                }
8475            }
8476        }
8477
8478        Err(runtime_err(format!(
8479            "Agent '{}' exceeded max_turns ({})",
8480            agent_def.name, agent_def.max_turns
8481        )))
8482    }
8483
8484    #[cfg(feature = "native")]
8485    fn execute_tool_call(
8486        &mut self,
8487        tool_name: &str,
8488        input: &serde_json::Value,
8489    ) -> Result<String, TlError> {
8490        // Look up the tool function in globals
8491        let func = self
8492            .globals
8493            .get(tool_name)
8494            .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8495            .clone();
8496
8497        // Convert JSON args to VmValues
8498        let args = self.json_to_vm_args(input);
8499
8500        // Call the function using call_value
8501        let result = self.call_value(func, &args)?;
8502
8503        // Convert result to string for the LLM
8504        Ok(format!("{result}"))
8505    }
8506
8507    #[cfg(feature = "native")]
8508    fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8509        match input {
8510            serde_json::Value::Object(map) => {
8511                // Pass values in order as positional args
8512                map.values().map(|v| self.json_value_to_vm(v)).collect()
8513            }
8514            serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8515            _ => vec![self.json_value_to_vm(input)],
8516        }
8517    }
8518
8519    #[cfg(feature = "native")]
8520    fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8521        match val {
8522            serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8523            serde_json::Value::Number(n) => {
8524                if let Some(i) = n.as_i64() {
8525                    VmValue::Int(i)
8526                } else if let Some(f) = n.as_f64() {
8527                    VmValue::Float(f)
8528                } else {
8529                    VmValue::None
8530                }
8531            }
8532            serde_json::Value::Bool(b) => VmValue::Bool(*b),
8533            serde_json::Value::Null => VmValue::None,
8534            serde_json::Value::Array(arr) => VmValue::List(Box::new(
8535                arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8536            )),
8537            serde_json::Value::Object(map) => {
8538                let pairs: Vec<(Arc<str>, VmValue)> = map
8539                    .iter()
8540                    .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8541                    .collect();
8542                VmValue::Map(Box::new(pairs))
8543            }
8544        }
8545    }
8546
8547    #[cfg(feature = "native")]
8548    fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8549        match &func {
8550            VmValue::Function(_) => {
8551                // Set up a synthetic call: push args to stack, do_call
8552                let save_len = self.stack.len();
8553                let func_slot = save_len;
8554                let _args_start = func_slot + 1;
8555                self.stack.push(func.clone());
8556                for arg in args {
8557                    self.stack.push(arg.clone());
8558                }
8559                self.ensure_stack(self.stack.len() + 256);
8560
8561                self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8562
8563                // Run until the function returns
8564                let entry_depth = self.frames.len() - 1;
8565                while self.frames.len() > entry_depth {
8566                    if self.run_step(entry_depth)?.is_some() {
8567                        break;
8568                    }
8569                }
8570
8571                // Result is at func_slot
8572                let result = self.stack[func_slot].clone();
8573                self.stack.truncate(save_len);
8574                Ok(result)
8575            }
8576            VmValue::Builtin(id) => {
8577                let id_u16 = *id as u16;
8578                let save_len = self.stack.len();
8579                for arg in args {
8580                    self.stack.push(arg.clone());
8581                }
8582                let result = self.call_builtin(id_u16, save_len, args.len())?;
8583                self.stack.truncate(save_len);
8584                Ok(result)
8585            }
8586            _ => Err(runtime_err(format!(
8587                "Agent tool '{}' is not callable",
8588                func.type_name()
8589            ))),
8590        }
8591    }
8592
8593    #[cfg(feature = "native")]
8594    fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8595        if let Some(dur) = s.strip_prefix("tumbling:") {
8596            let ms = tl_stream::parse_duration(dur).ok()?;
8597            Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8598        } else if let Some(rest) = s.strip_prefix("sliding:") {
8599            let parts: Vec<&str> = rest.splitn(2, ':').collect();
8600            if parts.len() == 2 {
8601                let wms = tl_stream::parse_duration(parts[0]).ok()?;
8602                let sms = tl_stream::parse_duration(parts[1]).ok()?;
8603                Some(tl_stream::window::WindowType::Sliding {
8604                    window_ms: wms,
8605                    slide_ms: sms,
8606                })
8607            } else {
8608                None
8609            }
8610        } else if let Some(dur) = s.strip_prefix("session:") {
8611            let ms = tl_stream::parse_duration(dur).ok()?;
8612            Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8613        } else {
8614            None
8615        }
8616    }
8617
8618    #[cfg(feature = "native")]
8619    fn handle_connector_decl(
8620        &mut self,
8621        frame_idx: usize,
8622        type_const: u8,
8623        config_const: u8,
8624    ) -> Result<VmValue, TlError> {
8625        let frame = &self.frames[frame_idx];
8626        let connector_type = match &frame.prototype.constants[type_const as usize] {
8627            Constant::String(s) => s.to_string(),
8628            _ => return Err(runtime_err("Expected string constant for connector type")),
8629        };
8630
8631        let config_args = match &frame.prototype.constants[config_const as usize] {
8632            Constant::AstExprList(args) => args.clone(),
8633            _ => return Err(runtime_err("Expected AST expr list for connector config")),
8634        };
8635
8636        let mut properties = std::collections::HashMap::new();
8637        for arg in &config_args {
8638            if let AstExpr::NamedArg { name: key, value } = arg {
8639                let val_str = match value.as_ref() {
8640                    AstExpr::String(s) => s.clone(),
8641                    AstExpr::Int(n) => n.to_string(),
8642                    AstExpr::Float(f) => f.to_string(),
8643                    AstExpr::Bool(b) => b.to_string(),
8644                    other => {
8645                        // Try to resolve Ident from globals
8646                        if let AstExpr::Ident(ident) = other {
8647                            if let Some(val) = self.globals.get(ident.as_str()) {
8648                                format!("{val}")
8649                            } else {
8650                                ident.clone()
8651                            }
8652                        } else {
8653                            format!("{other:?}")
8654                        }
8655                    }
8656                };
8657                properties.insert(key.clone(), val_str);
8658            }
8659        }
8660
8661        let config = tl_stream::ConnectorConfig {
8662            name: String::new(), // Will be set by SetGlobal
8663            connector_type,
8664            properties,
8665        };
8666
8667        Ok(VmValue::Connector(Arc::new(config)))
8668    }
8669
8670    /// Advance a generator by one step, returning the next value or None if done.
8671    fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8672        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8673        if gn.done {
8674            return Ok(VmValue::None);
8675        }
8676        match &mut gn.kind {
8677            GeneratorKind::UserDefined {
8678                prototype,
8679                upvalues,
8680                saved_stack,
8681                ip,
8682            } => {
8683                let proto = prototype.clone();
8684                let uvs = upvalues.clone();
8685                let stack_snapshot = saved_stack.clone();
8686                let saved_ip = *ip;
8687                drop(gn); // release lock before running VM
8688
8689                // Set up a frame to resume the generator
8690                let new_base = self.stack.len();
8691                let num_regs = proto.num_registers as usize;
8692                self.ensure_stack(new_base + num_regs + 1);
8693                // Restore saved registers
8694                for (i, val) in stack_snapshot.iter().enumerate() {
8695                    self.stack[new_base + i] = val.clone();
8696                }
8697
8698                self.frames.push(CallFrame {
8699                    prototype: proto,
8700                    ip: saved_ip,
8701                    base: new_base,
8702                    upvalues: uvs,
8703                });
8704
8705                self.yielded_value = None;
8706                let _result = self.run()?;
8707
8708                if let Some(yielded) = self.yielded_value.take() {
8709                    // Generator yielded — save state back
8710                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8711                    if let GeneratorKind::UserDefined {
8712                        saved_stack, ip, ..
8713                    } = &mut gn.kind
8714                    {
8715                        // Save the current register state
8716                        let num_regs_save = saved_stack.len();
8717                        for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8718                            if new_base + i < self.stack.len() {
8719                                *slot = self.stack[new_base + i].clone();
8720                            }
8721                        }
8722                        // Save the ip (instruction after yield)
8723                        *ip = self.yielded_ip;
8724                    }
8725                    self.stack.truncate(new_base);
8726                    Ok(yielded)
8727                } else {
8728                    // Generator returned (no yield) — mark done
8729                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8730                    gn.done = true;
8731                    self.stack.truncate(new_base);
8732                    Ok(VmValue::None)
8733                }
8734            }
8735            GeneratorKind::ListIter { items, index } => {
8736                if *index < items.len() {
8737                    let val = items[*index].clone();
8738                    *index += 1;
8739                    Ok(val)
8740                } else {
8741                    gn.done = true;
8742                    Ok(VmValue::None)
8743                }
8744            }
8745            GeneratorKind::Take { source, remaining } => {
8746                if *remaining == 0 {
8747                    gn.done = true;
8748                    return Ok(VmValue::None);
8749                }
8750                *remaining -= 1;
8751                let src = source.clone();
8752                drop(gn);
8753                let val = self.generator_next(&src)?;
8754                if matches!(val, VmValue::None) {
8755                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8756                    gn.done = true;
8757                }
8758                Ok(val)
8759            }
8760            GeneratorKind::Skip { source, remaining } => {
8761                let src = source.clone();
8762                let skip_n = *remaining;
8763                *remaining = 0;
8764                drop(gn);
8765                // Skip initial values
8766                for _ in 0..skip_n {
8767                    let val = self.generator_next(&src)?;
8768                    if matches!(val, VmValue::None) {
8769                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8770                        gn.done = true;
8771                        return Ok(VmValue::None);
8772                    }
8773                }
8774                let val = self.generator_next(&src)?;
8775                if matches!(val, VmValue::None) {
8776                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8777                    gn.done = true;
8778                }
8779                Ok(val)
8780            }
8781            GeneratorKind::Map { source, func } => {
8782                let src = source.clone();
8783                let f = func.clone();
8784                drop(gn);
8785                let val = self.generator_next(&src)?;
8786                if matches!(val, VmValue::None) {
8787                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8788                    gn.done = true;
8789                    return Ok(VmValue::None);
8790                }
8791                self.call_vm_function(&f, &[val])
8792            }
8793            GeneratorKind::Filter { source, func } => {
8794                let src = source.clone();
8795                let f = func.clone();
8796                drop(gn);
8797                loop {
8798                    let val = self.generator_next(&src)?;
8799                    if matches!(val, VmValue::None) {
8800                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8801                        gn.done = true;
8802                        return Ok(VmValue::None);
8803                    }
8804                    let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8805                    if test.is_truthy() {
8806                        return Ok(val);
8807                    }
8808                }
8809            }
8810            GeneratorKind::Chain {
8811                first,
8812                second,
8813                on_second,
8814            } => {
8815                if !*on_second {
8816                    let src = first.clone();
8817                    drop(gn);
8818                    let val = self.generator_next(&src)?;
8819                    if matches!(val, VmValue::None) {
8820                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8821                        if let GeneratorKind::Chain {
8822                            on_second, second, ..
8823                        } = &mut gn.kind
8824                        {
8825                            *on_second = true;
8826                            let src2 = second.clone();
8827                            drop(gn);
8828                            return self.generator_next(&src2);
8829                        }
8830                    }
8831                    Ok(val)
8832                } else {
8833                    let src = second.clone();
8834                    drop(gn);
8835                    let val = self.generator_next(&src)?;
8836                    if matches!(val, VmValue::None) {
8837                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8838                        gn.done = true;
8839                    }
8840                    Ok(val)
8841                }
8842            }
8843            GeneratorKind::Zip { first, second } => {
8844                let src1 = first.clone();
8845                let src2 = second.clone();
8846                drop(gn);
8847                let val1 = self.generator_next(&src1)?;
8848                let val2 = self.generator_next(&src2)?;
8849                if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8850                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8851                    gn.done = true;
8852                    return Ok(VmValue::None);
8853                }
8854                Ok(VmValue::List(Box::new(vec![val1, val2])))
8855            }
8856            GeneratorKind::Enumerate { source, index } => {
8857                let src = source.clone();
8858                let idx = *index;
8859                *index += 1;
8860                drop(gn);
8861                let val = self.generator_next(&src)?;
8862                if matches!(val, VmValue::None) {
8863                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8864                    gn.done = true;
8865                    return Ok(VmValue::None);
8866                }
8867                Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8868            }
8869        }
8870    }
8871
8872    /// Process a __schema__:Name:vN:fields... global to register in schema_registry.
8873    #[cfg(feature = "native")]
8874    fn process_schema_global(&mut self, s: &str) {
8875        // Format: __schema__:Name:vN:field1:Type,field2:Type,...
8876        let rest = &s["__schema__:".len()..];
8877        let parts: Vec<&str> = rest.splitn(3, ':').collect();
8878        if parts.len() < 2 {
8879            return;
8880        }
8881
8882        let schema_name = parts[0];
8883        let mut version: i64 = 0;
8884        let fields_str;
8885
8886        if parts.len() == 3 && parts[1].starts_with('v') {
8887            // Versioned: Name:vN:fields
8888            version = parts[1][1..].parse().unwrap_or(0);
8889            fields_str = parts[2];
8890        } else if parts.len() == 3 {
8891            // No version prefix, treat as v0: Name:field1:...
8892            fields_str = &rest[schema_name.len() + 1..];
8893        } else {
8894            fields_str = parts[1];
8895        }
8896
8897        if version == 0 {
8898            return;
8899        } // Only register versioned schemas
8900
8901        let mut arrow_fields = Vec::new();
8902        for field_pair in fields_str.split(',') {
8903            let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8904            if kv.len() == 2 {
8905                let fname = kv[0].trim();
8906                let ftype = kv[1].trim();
8907                // Parse type expr debug format: Simple("typename")
8908                let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8909                    &ftype[8..ftype.len() - 2]
8910                } else {
8911                    ftype
8912                };
8913                let dt = crate::schema::type_name_to_arrow_pub(type_name);
8914                arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8915            }
8916        }
8917
8918        if !arrow_fields.is_empty() {
8919            let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8920            let _ = self.schema_registry.register(
8921                schema_name,
8922                version,
8923                schema,
8924                crate::schema::SchemaMetadata::default(),
8925            );
8926        }
8927    }
8928
8929    /// Process a __migrate__:Name:fromVer:toVer:ops global to apply migration.
8930    #[cfg(feature = "native")]
8931    fn process_migrate_global(&mut self, s: &str) {
8932        // Format: __migrate__:Name:from:to:op1;op2;...
8933        let rest = &s["__migrate__:".len()..];
8934        let parts: Vec<&str> = rest.splitn(4, ':').collect();
8935        if parts.len() < 4 {
8936            return;
8937        }
8938
8939        let schema_name = parts[0];
8940        let from_ver: i64 = parts[1].parse().unwrap_or(0);
8941        let to_ver: i64 = parts[2].parse().unwrap_or(0);
8942        let ops_str = parts[3];
8943
8944        let mut ops = Vec::new();
8945        for op_str in ops_str.split(';') {
8946            let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8947            if op_parts.is_empty() {
8948                continue;
8949            }
8950            match op_parts[0] {
8951                "add" if op_parts.len() >= 3 => {
8952                    let name = op_parts[1].to_string();
8953                    // Parse type from debug format: Simple("typename")
8954                    let type_raw = op_parts[2];
8955                    let type_name =
8956                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8957                            type_raw[8..type_raw.len() - 2].to_string()
8958                        } else {
8959                            type_raw.to_string()
8960                        };
8961                    let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
8962                        Some(
8963                            op_parts[3]["default:".len()..]
8964                                .trim_matches('"')
8965                                .to_string(),
8966                        )
8967                    } else {
8968                        None
8969                    };
8970                    ops.push(crate::schema::MigrationOp::AddColumn {
8971                        name,
8972                        type_name,
8973                        default,
8974                    });
8975                }
8976                "drop" if op_parts.len() >= 2 => {
8977                    ops.push(crate::schema::MigrationOp::DropColumn {
8978                        name: op_parts[1].to_string(),
8979                    });
8980                }
8981                "rename" if op_parts.len() >= 3 => {
8982                    ops.push(crate::schema::MigrationOp::RenameColumn {
8983                        from: op_parts[1].to_string(),
8984                        to: op_parts[2].to_string(),
8985                    });
8986                }
8987                "alter" if op_parts.len() >= 3 => {
8988                    let type_raw = op_parts[2];
8989                    let type_name =
8990                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8991                            type_raw[8..type_raw.len() - 2].to_string()
8992                        } else {
8993                            type_raw.to_string()
8994                        };
8995                    ops.push(crate::schema::MigrationOp::AlterType {
8996                        column: op_parts[1].to_string(),
8997                        new_type: type_name,
8998                    });
8999                }
9000                _ => {}
9001            }
9002        }
9003
9004        let _ = self
9005            .schema_registry
9006            .apply_migration(schema_name, from_ver, to_ver, &ops);
9007    }
9008
9009    /// Dispatch a method call on an object.
9010    /// Deep-clone a VmValue, recursively copying containers.
9011    fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
9012        match val {
9013            VmValue::List(items) => {
9014                let cloned: Result<Vec<_>, _> =
9015                    items.iter().map(|v| self.deep_clone_value(v)).collect();
9016                Ok(VmValue::List(Box::new(cloned?)))
9017            }
9018            VmValue::Map(pairs) => {
9019                let cloned: Result<Vec<_>, _> = pairs
9020                    .iter()
9021                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9022                    .collect();
9023                Ok(VmValue::Map(Box::new(cloned?)))
9024            }
9025            VmValue::Set(items) => {
9026                let cloned: Result<Vec<_>, _> =
9027                    items.iter().map(|v| self.deep_clone_value(v)).collect();
9028                Ok(VmValue::Set(Box::new(cloned?)))
9029            }
9030            VmValue::StructInstance(inst) => {
9031                let cloned_fields: Result<Vec<_>, _> = inst
9032                    .fields
9033                    .iter()
9034                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9035                    .collect();
9036                Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
9037                    type_name: inst.type_name.clone(),
9038                    fields: cloned_fields?,
9039                })))
9040            }
9041            VmValue::EnumInstance(e) => {
9042                let cloned_fields: Result<Vec<_>, _> =
9043                    e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
9044                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
9045                    type_name: e.type_name.clone(),
9046                    variant: e.variant.clone(),
9047                    fields: cloned_fields?,
9048                })))
9049            }
9050            #[cfg(feature = "gpu")]
9051            VmValue::GpuTensor(gt) => {
9052                let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
9053                Ok(VmValue::GpuTensor(Arc::new(cloned)))
9054            }
9055            VmValue::Ref(inner) => self.deep_clone_value(inner),
9056            VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
9057            VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
9058            VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
9059            VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
9060            other => Ok(other.clone()),
9061        }
9062    }
9063
9064    pub fn dispatch_method(
9065        &mut self,
9066        obj: VmValue,
9067        method: &str,
9068        args: &[VmValue],
9069    ) -> Result<VmValue, TlError> {
9070        // Universal .clone() method — deep copy any value
9071        if method == "clone" {
9072            return self.deep_clone_value(&obj);
9073        }
9074        // Unwrap Ref for method dispatch — methods can be called through references
9075        let obj = match obj {
9076            VmValue::Ref(inner) => inner.as_ref().clone(),
9077            other => other,
9078        };
9079        match &obj {
9080            VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
9081            VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
9082            VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
9083            VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
9084            VmValue::Module(m) => {
9085                if let Some(func) = m.exports.get(method).cloned() {
9086                    self.call_vm_function(&func, args)
9087                } else {
9088                    Err(runtime_err(format!(
9089                        "Module '{}' has no export '{}'",
9090                        m.name, method
9091                    )))
9092                }
9093            }
9094            VmValue::StructInstance(inst) => {
9095                // Look up impl method: Type::method in globals
9096                let mangled = format!("{}::{}", inst.type_name, method);
9097                if let Some(func) = self.globals.get(&mangled).cloned() {
9098                    let mut all_args = vec![obj.clone()];
9099                    all_args.extend_from_slice(args);
9100                    self.call_vm_function(&func, &all_args)
9101                } else {
9102                    Err(runtime_err(format!(
9103                        "No method '{}' on struct '{}'",
9104                        method, inst.type_name
9105                    )))
9106                }
9107            }
9108            #[cfg(feature = "python")]
9109            VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
9110            #[cfg(feature = "gpu")]
9111            VmValue::GpuTensor(gt) => match method {
9112                "to_cpu" => {
9113                    let cpu = gt.to_cpu().map_err(runtime_err)?;
9114                    Ok(VmValue::Tensor(Arc::new(cpu)))
9115                }
9116                "shape" => {
9117                    let shape_list = Box::new(
9118                        gt.shape
9119                            .iter()
9120                            .map(|&d| VmValue::Int(d as i64))
9121                            .collect::<Vec<_>>(),
9122                    );
9123                    Ok(VmValue::List(shape_list))
9124                }
9125                "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
9126                _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
9127            },
9128            _ => {
9129                // Try looking up Type::method from type_name
9130                let type_name = obj.type_name();
9131                let mangled = format!("{}::{}", type_name, method);
9132                if let Some(func) = self.globals.get(&mangled).cloned() {
9133                    let mut all_args = vec![obj];
9134                    all_args.extend_from_slice(args);
9135                    self.call_vm_function(&func, &all_args)
9136                } else {
9137                    Err(runtime_err(format!(
9138                        "No method '{}' on type '{}'",
9139                        method, type_name
9140                    )))
9141                }
9142            }
9143        }
9144    }
9145
9146    /// Dispatch string methods.
9147    fn dispatch_string_method(
9148        &self,
9149        s: Arc<str>,
9150        method: &str,
9151        args: &[VmValue],
9152    ) -> Result<VmValue, TlError> {
9153        match method {
9154            "len" => Ok(VmValue::Int(s.len() as i64)),
9155            "split" => {
9156                let sep = match args.first() {
9157                    Some(VmValue::String(sep)) => sep.to_string(),
9158                    _ => return Err(runtime_err("split() expects a string separator")),
9159                };
9160                let parts: Vec<VmValue> = s
9161                    .split(&sep)
9162                    .map(|p| VmValue::String(Arc::from(p)))
9163                    .collect();
9164                Ok(VmValue::List(Box::new(parts)))
9165            }
9166            "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
9167            "contains" => {
9168                let needle = match args.first() {
9169                    Some(VmValue::String(n)) => n.to_string(),
9170                    _ => return Err(runtime_err("contains() expects a string")),
9171                };
9172                Ok(VmValue::Bool(s.contains(&needle)))
9173            }
9174            "replace" => {
9175                if args.len() < 2 {
9176                    return Err(runtime_err("replace() expects 2 arguments (old, new)"));
9177                }
9178                let old = match &args[0] {
9179                    VmValue::String(s) => s.to_string(),
9180                    _ => return Err(runtime_err("replace() arg must be string")),
9181                };
9182                let new = match &args[1] {
9183                    VmValue::String(s) => s.to_string(),
9184                    _ => return Err(runtime_err("replace() arg must be string")),
9185                };
9186                Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
9187            }
9188            "starts_with" => {
9189                let prefix = match args.first() {
9190                    Some(VmValue::String(p)) => p.to_string(),
9191                    _ => return Err(runtime_err("starts_with() expects a string")),
9192                };
9193                Ok(VmValue::Bool(s.starts_with(&prefix)))
9194            }
9195            "ends_with" => {
9196                let suffix = match args.first() {
9197                    Some(VmValue::String(p)) => p.to_string(),
9198                    _ => return Err(runtime_err("ends_with() expects a string")),
9199                };
9200                Ok(VmValue::Bool(s.ends_with(&suffix)))
9201            }
9202            "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
9203            "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
9204            "chars" => {
9205                let chars: Vec<VmValue> = s
9206                    .chars()
9207                    .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
9208                    .collect();
9209                Ok(VmValue::List(Box::new(chars)))
9210            }
9211            "repeat" => {
9212                let n = match args.first() {
9213                    Some(VmValue::Int(n)) => *n as usize,
9214                    _ => return Err(runtime_err("repeat() expects an integer")),
9215                };
9216                Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
9217            }
9218            "index_of" => {
9219                let needle = match args.first() {
9220                    Some(VmValue::String(n)) => n.to_string(),
9221                    _ => return Err(runtime_err("index_of() expects a string")),
9222                };
9223                Ok(VmValue::Int(
9224                    s.find(&needle).map(|i| i as i64).unwrap_or(-1),
9225                ))
9226            }
9227            "substring" => {
9228                if args.len() < 2 {
9229                    return Err(runtime_err("substring() expects start and end"));
9230                }
9231                let start = match &args[0] {
9232                    VmValue::Int(n) => *n as usize,
9233                    _ => return Err(runtime_err("substring() expects integers")),
9234                };
9235                let end = match &args[1] {
9236                    VmValue::Int(n) => *n as usize,
9237                    _ => return Err(runtime_err("substring() expects integers")),
9238                };
9239                let end = end.min(s.len());
9240                let start = start.min(end);
9241                Ok(VmValue::String(Arc::from(&s[start..end])))
9242            }
9243            "pad_left" => {
9244                if args.is_empty() {
9245                    return Err(runtime_err("pad_left() expects width"));
9246                }
9247                let width = match &args[0] {
9248                    VmValue::Int(n) => *n as usize,
9249                    _ => return Err(runtime_err("pad_left() expects integer width")),
9250                };
9251                let ch = match args.get(1) {
9252                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9253                    _ => ' ',
9254                };
9255                if s.len() >= width {
9256                    Ok(VmValue::String(s))
9257                } else {
9258                    Ok(VmValue::String(Arc::from(
9259                        format!(
9260                            "{}{}",
9261                            std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
9262                            s
9263                        )
9264                        .as_str(),
9265                    )))
9266                }
9267            }
9268            "pad_right" => {
9269                if args.is_empty() {
9270                    return Err(runtime_err("pad_right() expects width"));
9271                }
9272                let width = match &args[0] {
9273                    VmValue::Int(n) => *n as usize,
9274                    _ => return Err(runtime_err("pad_right() expects integer width")),
9275                };
9276                let ch = match args.get(1) {
9277                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9278                    _ => ' ',
9279                };
9280                if s.len() >= width {
9281                    Ok(VmValue::String(s))
9282                } else {
9283                    Ok(VmValue::String(Arc::from(
9284                        format!(
9285                            "{}{}",
9286                            s,
9287                            std::iter::repeat_n(ch, width - s.len()).collect::<String>()
9288                        )
9289                        .as_str(),
9290                    )))
9291                }
9292            }
9293            "join" => {
9294                // "sep".join(list) -> string
9295                let items = match args.first() {
9296                    Some(VmValue::List(items)) => items,
9297                    _ => return Err(runtime_err("join() expects a list")),
9298                };
9299                let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
9300                Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
9301            }
9302            "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
9303            "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
9304            "count" => {
9305                if args.is_empty() {
9306                    return Err(runtime_err("count() expects a substring"));
9307                }
9308                if let VmValue::String(sub) = &args[0] {
9309                    Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
9310                } else {
9311                    Err(runtime_err("count() expects a string"))
9312                }
9313            }
9314            "is_empty" => Ok(VmValue::Bool(s.is_empty())),
9315            "is_numeric" => Ok(VmValue::Bool(
9316                s.chars()
9317                    .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
9318            )),
9319            "is_alpha" => Ok(VmValue::Bool(
9320                !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
9321            )),
9322            "strip_prefix" => {
9323                if args.is_empty() {
9324                    return Err(runtime_err("strip_prefix() expects a string"));
9325                }
9326                if let VmValue::String(prefix) = &args[0] {
9327                    match s.strip_prefix(prefix.as_ref()) {
9328                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9329                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9330                    }
9331                } else {
9332                    Err(runtime_err("strip_prefix() expects a string"))
9333                }
9334            }
9335            "strip_suffix" => {
9336                if args.is_empty() {
9337                    return Err(runtime_err("strip_suffix() expects a string"));
9338                }
9339                if let VmValue::String(suffix) = &args[0] {
9340                    match s.strip_suffix(suffix.as_ref()) {
9341                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9342                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9343                    }
9344                } else {
9345                    Err(runtime_err("strip_suffix() expects a string"))
9346                }
9347            }
9348            _ => Err(runtime_err(format!("No method '{}' on string", method))),
9349        }
9350    }
9351
9352    /// Dispatch list methods.
9353    fn dispatch_list_method(
9354        &mut self,
9355        items: Vec<VmValue>,
9356        method: &str,
9357        args: &[VmValue],
9358    ) -> Result<VmValue, TlError> {
9359        match method {
9360            "len" => Ok(VmValue::Int(items.len() as i64)),
9361            "push" => {
9362                if args.is_empty() {
9363                    return Err(runtime_err("push() expects 1 argument"));
9364                }
9365                let mut new_items = items;
9366                new_items.push(args[0].clone());
9367                Ok(VmValue::List(Box::new(new_items)))
9368            }
9369            "map" => {
9370                if args.is_empty() {
9371                    return Err(runtime_err("map() expects a function"));
9372                }
9373                let func = &args[0];
9374                let mut result = Vec::new();
9375                for item in items {
9376                    let val = self.call_vm_function(func, &[item])?;
9377                    result.push(val);
9378                }
9379                Ok(VmValue::List(Box::new(result)))
9380            }
9381            "filter" => {
9382                if args.is_empty() {
9383                    return Err(runtime_err("filter() expects a function"));
9384                }
9385                let func = &args[0];
9386                let mut result = Vec::new();
9387                for item in items {
9388                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9389                    if val.is_truthy() {
9390                        result.push(item);
9391                    }
9392                }
9393                Ok(VmValue::List(Box::new(result)))
9394            }
9395            "reduce" => {
9396                if args.len() < 2 {
9397                    return Err(runtime_err("reduce() expects initial value and function"));
9398                }
9399                let mut acc = args[0].clone();
9400                let func = &args[1];
9401                for item in items {
9402                    acc = self.call_vm_function(func, &[acc, item])?;
9403                }
9404                Ok(acc)
9405            }
9406            "sort" => {
9407                let mut sorted = items;
9408                sorted.sort_by(|a, b| match (a, b) {
9409                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9410                    (VmValue::Float(x), VmValue::Float(y)) => {
9411                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9412                    }
9413                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9414                    _ => std::cmp::Ordering::Equal,
9415                });
9416                Ok(VmValue::List(Box::new(sorted)))
9417            }
9418            "reverse" => {
9419                let mut reversed = items;
9420                reversed.reverse();
9421                Ok(VmValue::List(Box::new(reversed)))
9422            }
9423            "contains" => {
9424                if args.is_empty() {
9425                    return Err(runtime_err("contains() expects a value"));
9426                }
9427                let needle = &args[0];
9428                let found = items.iter().any(|item| match (item, needle) {
9429                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
9430                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
9431                    (VmValue::String(a), VmValue::String(b)) => a == b,
9432                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9433                    (VmValue::None, VmValue::None) => true,
9434                    _ => false,
9435                });
9436                Ok(VmValue::Bool(found))
9437            }
9438            "index_of" => {
9439                if args.is_empty() {
9440                    return Err(runtime_err("index_of() expects a value"));
9441                }
9442                let needle = &args[0];
9443                let idx = items.iter().position(|item| match (item, needle) {
9444                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
9445                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
9446                    (VmValue::String(a), VmValue::String(b)) => a == b,
9447                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9448                    (VmValue::None, VmValue::None) => true,
9449                    _ => false,
9450                });
9451                Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
9452            }
9453            "slice" => {
9454                if args.len() < 2 {
9455                    return Err(runtime_err("slice() expects start and end"));
9456                }
9457                let start = match &args[0] {
9458                    VmValue::Int(n) => *n as usize,
9459                    _ => return Err(runtime_err("slice() expects integers")),
9460                };
9461                let end = match &args[1] {
9462                    VmValue::Int(n) => *n as usize,
9463                    _ => return Err(runtime_err("slice() expects integers")),
9464                };
9465                let end = end.min(items.len());
9466                let start = start.min(end);
9467                Ok(VmValue::List(Box::new(items[start..end].to_vec())))
9468            }
9469            "flat_map" => {
9470                if args.is_empty() {
9471                    return Err(runtime_err("flat_map() expects a function"));
9472                }
9473                let func = &args[0];
9474                let mut result = Vec::new();
9475                for item in items {
9476                    let val = self.call_vm_function(func, &[item])?;
9477                    match val {
9478                        VmValue::List(sub) => result.extend(*sub),
9479                        other => result.push(other),
9480                    }
9481                }
9482                Ok(VmValue::List(Box::new(result)))
9483            }
9484            "find" => {
9485                if args.is_empty() {
9486                    return Err(runtime_err("find() expects a predicate function"));
9487                }
9488                let func = &args[0];
9489                for item in items {
9490                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9491                    if val.is_truthy() {
9492                        return Ok(item);
9493                    }
9494                }
9495                Ok(VmValue::None)
9496            }
9497            "sort_by" => {
9498                if args.is_empty() {
9499                    return Err(runtime_err("sort_by() expects a key function"));
9500                }
9501                let func = &args[0];
9502                let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9503                for item in items {
9504                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9505                    keyed.push((key, item));
9506                }
9507                keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9508                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9509                    (VmValue::Float(x), VmValue::Float(y)) => {
9510                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9511                    }
9512                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9513                    _ => std::cmp::Ordering::Equal,
9514                });
9515                Ok(VmValue::List(Box::new(
9516                    keyed.into_iter().map(|(_, v)| v).collect(),
9517                )))
9518            }
9519            "group_by" => {
9520                if args.is_empty() {
9521                    return Err(runtime_err("group_by() expects a key function"));
9522                }
9523                let func = &args[0];
9524                let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9525                for item in items {
9526                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9527                    let key_str: Arc<str> = match &key {
9528                        VmValue::String(s) => s.clone(),
9529                        other => Arc::from(format!("{other}").as_str()),
9530                    };
9531                    if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9532                        group.1.push(item);
9533                    } else {
9534                        groups.push((key_str, vec![item]));
9535                    }
9536                }
9537                let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9538                    .into_iter()
9539                    .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9540                    .collect();
9541                Ok(VmValue::Map(Box::new(map_pairs)))
9542            }
9543            "unique" => {
9544                let mut seen = Vec::new();
9545                let mut result = Vec::new();
9546                for item in &items {
9547                    let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9548                    if !is_dup {
9549                        seen.push(item.clone());
9550                        result.push(item.clone());
9551                    }
9552                }
9553                Ok(VmValue::List(Box::new(result)))
9554            }
9555            "flatten" => {
9556                let mut result = Vec::new();
9557                for item in items {
9558                    match item {
9559                        VmValue::List(sub) => result.extend(*sub),
9560                        other => result.push(other),
9561                    }
9562                }
9563                Ok(VmValue::List(Box::new(result)))
9564            }
9565            "chunk" => {
9566                if args.is_empty() {
9567                    return Err(runtime_err("chunk() expects a size"));
9568                }
9569                let n = match &args[0] {
9570                    VmValue::Int(n) if *n > 0 => *n as usize,
9571                    _ => return Err(runtime_err("chunk() expects a positive integer")),
9572                };
9573                let chunks: Vec<VmValue> = items
9574                    .chunks(n)
9575                    .map(|c| VmValue::List(Box::new(c.to_vec())))
9576                    .collect();
9577                Ok(VmValue::List(Box::new(chunks)))
9578            }
9579            "insert" => {
9580                if args.len() < 2 {
9581                    return Err(runtime_err("insert() expects index and value"));
9582                }
9583                let idx = match &args[0] {
9584                    VmValue::Int(n) => *n as usize,
9585                    _ => return Err(runtime_err("insert() expects integer index")),
9586                };
9587                let mut new_items = items;
9588                if idx > new_items.len() {
9589                    return Err(runtime_err("insert() index out of bounds"));
9590                }
9591                new_items.insert(idx, args[1].clone());
9592                Ok(VmValue::List(Box::new(new_items)))
9593            }
9594            "remove_at" => {
9595                if args.is_empty() {
9596                    return Err(runtime_err("remove_at() expects an index"));
9597                }
9598                let idx = match &args[0] {
9599                    VmValue::Int(n) => *n as usize,
9600                    _ => return Err(runtime_err("remove_at() expects integer index")),
9601                };
9602                let mut new_items = items;
9603                if idx >= new_items.len() {
9604                    return Err(runtime_err("remove_at() index out of bounds"));
9605                }
9606                let removed = new_items.remove(idx);
9607                Ok(removed)
9608            }
9609            "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9610            "sum" => {
9611                let mut int_sum: i64 = 0;
9612                let mut has_float = false;
9613                let mut float_sum: f64 = 0.0;
9614                for item in &items {
9615                    match item {
9616                        VmValue::Int(n) => {
9617                            if has_float {
9618                                float_sum += *n as f64;
9619                            } else {
9620                                int_sum += n;
9621                            }
9622                        }
9623                        VmValue::Float(f) => {
9624                            if !has_float {
9625                                has_float = true;
9626                                float_sum = int_sum as f64;
9627                            }
9628                            float_sum += f;
9629                        }
9630                        _ => return Err(runtime_err("sum() requires numeric list")),
9631                    }
9632                }
9633                if has_float {
9634                    Ok(VmValue::Float(float_sum))
9635                } else {
9636                    Ok(VmValue::Int(int_sum))
9637                }
9638            }
9639            "min" => {
9640                if items.is_empty() {
9641                    return Ok(VmValue::None);
9642                }
9643                let mut min_val = items[0].clone();
9644                for item in &items[1..] {
9645                    match (&min_val, item) {
9646                        (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9647                        (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9648                        _ => {}
9649                    }
9650                }
9651                Ok(min_val)
9652            }
9653            "max" => {
9654                if items.is_empty() {
9655                    return Ok(VmValue::None);
9656                }
9657                let mut max_val = items[0].clone();
9658                for item in &items[1..] {
9659                    match (&max_val, item) {
9660                        (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9661                        (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9662                        _ => {}
9663                    }
9664                }
9665                Ok(max_val)
9666            }
9667            "each" => {
9668                if args.is_empty() {
9669                    return Err(runtime_err("each() expects a function"));
9670                }
9671                let func = &args[0];
9672                for item in items {
9673                    self.call_vm_function(func, &[item])?;
9674                }
9675                Ok(VmValue::None)
9676            }
9677            "zip" => {
9678                if args.is_empty() {
9679                    return Err(runtime_err("zip() expects a list"));
9680                }
9681                let other = match &args[0] {
9682                    VmValue::List(other) => other.as_slice(),
9683                    _ => return Err(runtime_err("zip() expects a list")),
9684                };
9685                let len = items.len().min(other.len());
9686                let zipped: Vec<VmValue> = items[..len]
9687                    .iter()
9688                    .zip(other[..len].iter())
9689                    .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9690                    .collect();
9691                Ok(VmValue::List(Box::new(zipped)))
9692            }
9693            "join" => {
9694                let sep = match args.first() {
9695                    Some(VmValue::String(s)) => s.as_ref(),
9696                    _ => "",
9697                };
9698                let s: String = items
9699                    .iter()
9700                    .map(|v| format!("{v}"))
9701                    .collect::<Vec<_>>()
9702                    .join(sep);
9703                Ok(VmValue::String(Arc::from(s.as_str())))
9704            }
9705            _ => Err(runtime_err(format!("No method '{}' on list", method))),
9706        }
9707    }
9708
9709    /// Dispatch map methods.
9710    fn dispatch_map_method(
9711        &mut self,
9712        pairs: Vec<(Arc<str>, VmValue)>,
9713        method: &str,
9714        args: &[VmValue],
9715    ) -> Result<VmValue, TlError> {
9716        match method {
9717            "len" => Ok(VmValue::Int(pairs.len() as i64)),
9718            "keys" => Ok(VmValue::List(Box::new(
9719                pairs
9720                    .iter()
9721                    .map(|(k, _)| VmValue::String(k.clone()))
9722                    .collect(),
9723            ))),
9724            "values" => Ok(VmValue::List(Box::new(
9725                pairs.iter().map(|(_, v)| v.clone()).collect(),
9726            ))),
9727            "contains_key" => {
9728                if args.is_empty() {
9729                    return Err(runtime_err("contains_key() expects a key"));
9730                }
9731                if let VmValue::String(key) = &args[0] {
9732                    Ok(VmValue::Bool(
9733                        pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9734                    ))
9735                } else {
9736                    Err(runtime_err("contains_key() expects a string key"))
9737                }
9738            }
9739            "remove" => {
9740                if args.is_empty() {
9741                    return Err(runtime_err("remove() expects a key"));
9742                }
9743                if let VmValue::String(key) = &args[0] {
9744                    let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9745                        .into_iter()
9746                        .filter(|(k, _)| k.as_ref() != key.as_ref())
9747                        .collect();
9748                    Ok(VmValue::Map(Box::new(new_pairs)))
9749                } else {
9750                    Err(runtime_err("remove() expects a string key"))
9751                }
9752            }
9753            "get" => {
9754                if args.is_empty() {
9755                    return Err(runtime_err("get() expects a key"));
9756                }
9757                if let VmValue::String(key) = &args[0] {
9758                    let default = args.get(1).cloned().unwrap_or(VmValue::None);
9759                    let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9760                    Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9761                } else {
9762                    Err(runtime_err("get() expects a string key"))
9763                }
9764            }
9765            "merge" => {
9766                if args.is_empty() {
9767                    return Err(runtime_err("merge() expects a map"));
9768                }
9769                if let VmValue::Map(other) = &args[0] {
9770                    let mut merged = pairs;
9771                    for (k, v) in other.iter() {
9772                        if let Some(existing) =
9773                            merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9774                        {
9775                            existing.1 = v.clone();
9776                        } else {
9777                            merged.push((k.clone(), v.clone()));
9778                        }
9779                    }
9780                    Ok(VmValue::Map(Box::new(merged)))
9781                } else {
9782                    Err(runtime_err("merge() expects a map"))
9783                }
9784            }
9785            "entries" => {
9786                let entries: Vec<VmValue> = pairs
9787                    .iter()
9788                    .map(|(k, v)| {
9789                        VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9790                    })
9791                    .collect();
9792                Ok(VmValue::List(Box::new(entries)))
9793            }
9794            "map_values" => {
9795                if args.is_empty() {
9796                    return Err(runtime_err("map_values() expects a function"));
9797                }
9798                let func = &args[0];
9799                let mut result = Vec::new();
9800                for (k, v) in pairs {
9801                    let new_v = self.call_vm_function(func, &[v])?;
9802                    result.push((k, new_v));
9803                }
9804                Ok(VmValue::Map(Box::new(result)))
9805            }
9806            "filter" => {
9807                if args.is_empty() {
9808                    return Err(runtime_err("filter() expects a predicate function"));
9809                }
9810                let func = &args[0];
9811                let mut result = Vec::new();
9812                for (k, v) in pairs {
9813                    let val =
9814                        self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9815                    if val.is_truthy() {
9816                        result.push((k, v));
9817                    }
9818                }
9819                Ok(VmValue::Map(Box::new(result)))
9820            }
9821            "set" => {
9822                if args.len() < 2 {
9823                    return Err(runtime_err("set() expects key and value"));
9824                }
9825                if let VmValue::String(key) = &args[0] {
9826                    let mut new_pairs = pairs;
9827                    if let Some(existing) = new_pairs
9828                        .iter_mut()
9829                        .find(|(k, _)| k.as_ref() == key.as_ref())
9830                    {
9831                        existing.1 = args[1].clone();
9832                    } else {
9833                        new_pairs.push((key.clone(), args[1].clone()));
9834                    }
9835                    Ok(VmValue::Map(Box::new(new_pairs)))
9836                } else {
9837                    Err(runtime_err("set() expects a string key"))
9838                }
9839            }
9840            "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9841            _ => Err(runtime_err(format!("No method '{}' on map", method))),
9842        }
9843    }
9844
9845    /// Dispatch set methods.
9846    fn dispatch_set_method(
9847        &self,
9848        items: Vec<VmValue>,
9849        method: &str,
9850        args: &[VmValue],
9851    ) -> Result<VmValue, TlError> {
9852        match method {
9853            "len" => Ok(VmValue::Int(items.len() as i64)),
9854            "contains" => {
9855                if args.is_empty() {
9856                    return Err(runtime_err("contains() expects a value"));
9857                }
9858                Ok(VmValue::Bool(
9859                    items.iter().any(|x| vm_values_equal(x, &args[0])),
9860                ))
9861            }
9862            "add" => {
9863                if args.is_empty() {
9864                    return Err(runtime_err("add() expects a value"));
9865                }
9866                let mut new_items = items;
9867                if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9868                    new_items.push(args[0].clone());
9869                }
9870                Ok(VmValue::Set(Box::new(new_items)))
9871            }
9872            "remove" => {
9873                if args.is_empty() {
9874                    return Err(runtime_err("remove() expects a value"));
9875                }
9876                let new_items: Vec<VmValue> = items
9877                    .into_iter()
9878                    .filter(|x| !vm_values_equal(x, &args[0]))
9879                    .collect();
9880                Ok(VmValue::Set(Box::new(new_items)))
9881            }
9882            "to_list" => Ok(VmValue::List(Box::new(items))),
9883            "union" => {
9884                if args.is_empty() {
9885                    return Err(runtime_err("union() expects a set"));
9886                }
9887                if let VmValue::Set(b) = &args[0] {
9888                    let mut result = items;
9889                    for item in b.iter() {
9890                        if !result.iter().any(|x| vm_values_equal(x, item)) {
9891                            result.push(item.clone());
9892                        }
9893                    }
9894                    Ok(VmValue::Set(Box::new(result)))
9895                } else {
9896                    Err(runtime_err("union() expects a set"))
9897                }
9898            }
9899            "intersection" => {
9900                if args.is_empty() {
9901                    return Err(runtime_err("intersection() expects a set"));
9902                }
9903                if let VmValue::Set(b) = &args[0] {
9904                    let result: Vec<VmValue> = items
9905                        .into_iter()
9906                        .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9907                        .collect();
9908                    Ok(VmValue::Set(Box::new(result)))
9909                } else {
9910                    Err(runtime_err("intersection() expects a set"))
9911                }
9912            }
9913            "difference" => {
9914                if args.is_empty() {
9915                    return Err(runtime_err("difference() expects a set"));
9916                }
9917                if let VmValue::Set(b) = &args[0] {
9918                    let result: Vec<VmValue> = items
9919                        .into_iter()
9920                        .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9921                        .collect();
9922                    Ok(VmValue::Set(Box::new(result)))
9923                } else {
9924                    Err(runtime_err("difference() expects a set"))
9925                }
9926            }
9927            _ => Err(runtime_err(format!("No method '{}' on set", method))),
9928        }
9929    }
9930
9931    /// Handle import at runtime.
9932    #[cfg(feature = "native")]
9933    fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9934        // Resolve relative path from current file
9935        let resolved = if let Some(ref base) = self.file_path {
9936            let base_dir = std::path::Path::new(base)
9937                .parent()
9938                .unwrap_or(std::path::Path::new("."));
9939            let candidate = base_dir.join(path);
9940            if candidate.exists() {
9941                candidate.to_string_lossy().to_string()
9942            } else {
9943                path.to_string()
9944            }
9945        } else {
9946            path.to_string()
9947        };
9948
9949        // Circular dependency detection
9950        if self.importing_files.contains(&resolved) {
9951            return Err(runtime_err(format!("Circular import detected: {resolved}")));
9952        }
9953
9954        // Check module cache
9955        if let Some(exports) = self.module_cache.get(&resolved) {
9956            let exports = exports.clone();
9957            return self.bind_import_exports(exports, alias);
9958        }
9959
9960        // Read, parse, compile, execute the file
9961        let source = std::fs::read_to_string(&resolved)
9962            .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
9963        let program = tl_parser::parse(&source)
9964            .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
9965        let proto = crate::compiler::compile(&program)
9966            .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
9967
9968        // Track circular imports
9969        self.importing_files.insert(resolved.clone());
9970
9971        // Execute in a fresh VM with shared globals
9972        let mut import_vm = Vm::new();
9973        import_vm.file_path = Some(resolved.clone());
9974        import_vm.globals = self.globals.clone();
9975        import_vm.importing_files = self.importing_files.clone();
9976        import_vm.module_cache = self.module_cache.clone();
9977        import_vm.package_roots = self.package_roots.clone();
9978        import_vm.project_root = self.project_root.clone();
9979        import_vm.execute(&proto)?;
9980
9981        self.importing_files.remove(&resolved);
9982
9983        // Collect exports: both globals and top-level locals from the stack
9984        let mut exports = HashMap::new();
9985
9986        // 1. New globals defined in the import
9987        for (k, v) in &import_vm.globals {
9988            if !self.globals.contains_key(k) {
9989                exports.insert(k.clone(), v.clone());
9990            }
9991        }
9992
9993        // 2. Top-level locals from the prototype (on the stack)
9994        for (name, reg) in &proto.top_level_locals {
9995            if !name.starts_with("__enum_") && !exports.contains_key(name) {
9996                let stack_idx = reg;
9997                if (*stack_idx as usize) < import_vm.stack.len() {
9998                    let val = import_vm.stack[*stack_idx as usize].clone();
9999                    if !matches!(val, VmValue::None) || name.starts_with("_") {
10000                        exports.insert(name.clone(), val);
10001                    }
10002                }
10003            }
10004        }
10005
10006        // Cache the module
10007        self.module_cache.insert(resolved, exports.clone());
10008        // Also adopt any modules the sub-VM discovered
10009        for (k, v) in import_vm.module_cache {
10010            self.module_cache.entry(k).or_insert(v);
10011        }
10012
10013        self.bind_import_exports(exports, alias)
10014    }
10015
10016    /// Bind import exports into current scope.
10017    #[cfg(feature = "native")]
10018    fn bind_import_exports(
10019        &mut self,
10020        exports: HashMap<String, VmValue>,
10021        alias: &str,
10022    ) -> Result<VmValue, TlError> {
10023        if alias.is_empty() {
10024            // Wildcard import: merge all exports into current scope
10025            for (k, v) in &exports {
10026                self.globals.insert(k.clone(), v.clone());
10027            }
10028            Ok(VmValue::None)
10029        } else {
10030            // Namespaced import
10031            let module = VmModule {
10032                name: Arc::from(alias),
10033                exports,
10034            };
10035            let module_val = VmValue::Module(Arc::new(module));
10036            self.globals.insert(alias.to_string(), module_val.clone());
10037            Ok(module_val)
10038        }
10039    }
10040
10041    /// Handle use-style imports (dot-path syntax).
10042    #[cfg(feature = "native")]
10043    fn handle_use_import(
10044        &mut self,
10045        path_str: &str,
10046        extra_a: u8,
10047        kind: u8,
10048        _frame_idx: usize,
10049    ) -> Result<VmValue, TlError> {
10050        match kind {
10051            0 => {
10052                // Single: "data.transforms.clean" → import file, bind last segment
10053                let segments: Vec<&str> = path_str.split('.').collect();
10054                let file_path = self.resolve_use_path(&segments)?;
10055                // Import the module, get exports
10056                let _last = segments.last().copied().unwrap_or("");
10057                self.handle_import(&file_path, "")?;
10058                // The wildcard import already merged everything.
10059                // But for Single, we only want the specific item.
10060                // Since handle_import merges all, that works for now.
10061                // Return none since it's a statement, not an expression.
10062                Ok(VmValue::None)
10063            }
10064            1 => {
10065                // Group: "data.transforms.{a,b}" — extract prefix before {
10066                let brace_start = path_str.find('{').unwrap_or(path_str.len());
10067                let prefix = path_str[..brace_start].trim_end_matches('.');
10068                let segments: Vec<&str> = prefix.split('.').collect();
10069                let file_path = self.resolve_use_path(&segments)?;
10070                self.handle_import(&file_path, "")?;
10071                Ok(VmValue::None)
10072            }
10073            2 => {
10074                // Wildcard: "data.transforms.*" — strip trailing .*
10075                let prefix = path_str.trim_end_matches(".*");
10076                let segments: Vec<&str> = prefix.split('.').collect();
10077                let file_path = self.resolve_use_path(&segments)?;
10078                self.handle_import(&file_path, "")?;
10079                Ok(VmValue::None)
10080            }
10081            3 => {
10082                // Aliased: path in path_str, alias in extra_a (constant index)
10083                let segments: Vec<&str> = path_str.split('.').collect();
10084                let file_path = self.resolve_use_path(&segments)?;
10085                // For aliased, we need to get the alias from the constant pool
10086                // extra_a contains the constant index of the alias string
10087                let alias_str = if let Some(frame) = self.frames.last() {
10088                    if let Some(crate::chunk::Constant::String(s)) =
10089                        frame.prototype.constants.get(extra_a as usize)
10090                    {
10091                        s.to_string()
10092                    } else {
10093                        segments.last().copied().unwrap_or("module").to_string()
10094                    }
10095                } else {
10096                    segments.last().copied().unwrap_or("module").to_string()
10097                };
10098                self.handle_import(&file_path, &alias_str)?;
10099                Ok(VmValue::None)
10100            }
10101            _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
10102        }
10103    }
10104
10105    /// Resolve dot-path segments to a file path for use statements.
10106    #[cfg(feature = "native")]
10107    fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
10108        // Reject path traversal attempts
10109        if segments.contains(&"..") {
10110            return Err(runtime_err("Import paths cannot contain '..'"));
10111        }
10112
10113        let base_dir = if let Some(ref fp) = self.file_path {
10114            std::path::Path::new(fp)
10115                .parent()
10116                .unwrap_or(std::path::Path::new("."))
10117                .to_path_buf()
10118        } else {
10119            std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
10120        };
10121
10122        let rel_path = segments.join("/");
10123
10124        // Try file module first
10125        let file_path = base_dir.join(format!("{rel_path}.tl"));
10126        if file_path.exists() {
10127            return Ok(file_path.to_string_lossy().to_string());
10128        }
10129
10130        // Try directory module
10131        let dir_path = base_dir.join(&rel_path).join("mod.tl");
10132        if dir_path.exists() {
10133            return Ok(dir_path.to_string_lossy().to_string());
10134        }
10135
10136        // If multi-segment, try parent as file module
10137        if segments.len() > 1 {
10138            let parent = &segments[..segments.len() - 1];
10139            let parent_path = parent.join("/");
10140            let parent_file = base_dir.join(format!("{parent_path}.tl"));
10141            if parent_file.exists() {
10142                return Ok(parent_file.to_string_lossy().to_string());
10143            }
10144            let parent_dir = base_dir.join(&parent_path).join("mod.tl");
10145            if parent_dir.exists() {
10146                return Ok(parent_dir.to_string_lossy().to_string());
10147            }
10148        }
10149
10150        // Package import fallback: first segment as package name
10151        // Convert underscores to hyphens (TL identifiers use _, package names use -)
10152        let pkg_name_underscore = segments[0];
10153        let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
10154        let pkg_root = self
10155            .package_roots
10156            .get(pkg_name_underscore)
10157            .or_else(|| self.package_roots.get(&pkg_name_hyphen));
10158
10159        if let Some(root) = pkg_root {
10160            let remaining = &segments[1..];
10161            if let Some(path) = resolve_package_file(root, remaining) {
10162                return Ok(path);
10163            }
10164        }
10165
10166        Err(runtime_err(format!(
10167            "Module not found: `{}`",
10168            segments.join(".")
10169        )))
10170    }
10171
10172    /// Call a VmValue function/closure with args.
10173    fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
10174        match func {
10175            VmValue::Function(closure) => {
10176                let proto = closure.prototype.clone();
10177                let arity = proto.arity as usize;
10178                if args.len() != arity {
10179                    return Err(runtime_err(format!(
10180                        "Expected {} arguments, got {}",
10181                        arity,
10182                        args.len()
10183                    )));
10184                }
10185
10186                // If this is a generator function, create a Generator
10187                if proto.is_generator {
10188                    let mut closed_upvalues = Vec::new();
10189                    for uv in &closure.upvalues {
10190                        match uv {
10191                            UpvalueRef::Open { stack_index } => {
10192                                let val = self.stack[*stack_index].clone();
10193                                closed_upvalues.push(UpvalueRef::Closed(val));
10194                            }
10195                            UpvalueRef::Closed(v) => {
10196                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
10197                            }
10198                        }
10199                    }
10200                    let num_regs = proto.num_registers as usize;
10201                    let mut saved_stack = vec![VmValue::None; num_regs];
10202                    for (i, arg) in args.iter().enumerate() {
10203                        saved_stack[i] = arg.clone();
10204                    }
10205                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
10206                        prototype: proto,
10207                        upvalues: closed_upvalues,
10208                        saved_stack,
10209                        ip: 0,
10210                    });
10211                    return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
10212                }
10213
10214                let new_base = self.stack.len();
10215                self.ensure_stack(new_base + proto.num_registers as usize + 1);
10216
10217                for (i, arg) in args.iter().enumerate() {
10218                    self.stack[new_base + i] = arg.clone();
10219                }
10220
10221                self.frames.push(CallFrame {
10222                    prototype: proto,
10223                    ip: 0,
10224                    base: new_base,
10225                    upvalues: closure.upvalues.clone(),
10226                });
10227
10228                let result = self.run()?;
10229                self.stack.truncate(new_base);
10230                Ok(result)
10231            }
10232            VmValue::Builtin(id) => {
10233                // Put args on stack temporarily
10234                let args_base = self.stack.len();
10235                for arg in args {
10236                    self.stack.push(arg.clone());
10237                }
10238                let result = self.call_builtin(*id as u16, args_base, args.len());
10239                self.stack.truncate(args_base);
10240                result
10241            }
10242            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
10243        }
10244    }
10245
10246    // ── Table pipe handler ──
10247
10248    #[cfg(feature = "native")]
10249    fn handle_table_pipe(
10250        &mut self,
10251        frame_idx: usize,
10252        table_val: VmValue,
10253        op_const: u8,
10254        args_const: u8,
10255    ) -> Result<VmValue, TlError> {
10256        let df = match table_val {
10257            VmValue::Table(t) => t.df,
10258            other => {
10259                // Not a table — fall back to regular builtin/function call
10260                return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
10261            }
10262        };
10263
10264        let frame = &self.frames[frame_idx];
10265        let op_name = match &frame.prototype.constants[op_const as usize] {
10266            Constant::String(s) => s.to_string(),
10267            _ => return Err(runtime_err("Expected string constant for table op")),
10268        };
10269        let ast_args = match &frame.prototype.constants[args_const as usize] {
10270            Constant::AstExprList(args) => args.clone(),
10271            _ => return Err(runtime_err("Expected AST expr list for table args")),
10272        };
10273
10274        let ctx = self.build_translate_context();
10275
10276        match op_name.as_str() {
10277            "filter" => {
10278                if ast_args.len() != 1 {
10279                    return Err(runtime_err("filter() expects 1 argument (predicate)"));
10280                }
10281                let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
10282                let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
10283                Ok(VmValue::Table(VmTable { df: filtered }))
10284            }
10285            "select" => {
10286                if ast_args.is_empty() {
10287                    return Err(runtime_err("select() expects at least 1 argument"));
10288                }
10289                let mut select_exprs = Vec::new();
10290                for arg in &ast_args {
10291                    match arg {
10292                        AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
10293                        AstExpr::NamedArg { name, value } => {
10294                            let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10295                            select_exprs.push(expr.alias(name));
10296                        }
10297                        AstExpr::String(name) => select_exprs.push(col(name.as_str())),
10298                        other => {
10299                            let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10300                            select_exprs.push(expr);
10301                        }
10302                    }
10303                }
10304                let selected = df
10305                    .select(select_exprs)
10306                    .map_err(|e| runtime_err(format!("{e}")))?;
10307                Ok(VmValue::Table(VmTable { df: selected }))
10308            }
10309            "sort" => {
10310                if ast_args.is_empty() {
10311                    return Err(runtime_err("sort() expects at least 1 argument (column)"));
10312                }
10313                let mut sort_exprs = Vec::new();
10314                let mut i = 0;
10315                while i < ast_args.len() {
10316                    let col_name = match &ast_args[i] {
10317                        AstExpr::Ident(name) => name.clone(),
10318                        AstExpr::String(name) => name.clone(),
10319                        _ => {
10320                            return Err(runtime_err(
10321                                "sort() column must be an identifier or string",
10322                            ));
10323                        }
10324                    };
10325                    i += 1;
10326                    let ascending = if i < ast_args.len() {
10327                        match &ast_args[i] {
10328                            AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
10329                                i += 1;
10330                                false
10331                            }
10332                            AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
10333                                i += 1;
10334                                true
10335                            }
10336                            _ => true,
10337                        }
10338                    } else {
10339                        true
10340                    };
10341                    sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
10342                }
10343                let sorted = df
10344                    .sort(sort_exprs)
10345                    .map_err(|e| runtime_err(format!("{e}")))?;
10346                Ok(VmValue::Table(VmTable { df: sorted }))
10347            }
10348            "with" => {
10349                if ast_args.len() != 1 {
10350                    return Err(runtime_err(
10351                        "with() expects 1 argument (map of column definitions)",
10352                    ));
10353                }
10354                let pairs = match &ast_args[0] {
10355                    AstExpr::Map(pairs) => pairs,
10356                    _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
10357                };
10358                let mut result_df = df;
10359                for (key, value_expr) in pairs {
10360                    let col_name = match key {
10361                        AstExpr::String(s) => s.clone(),
10362                        AstExpr::Ident(s) => s.clone(),
10363                        _ => return Err(runtime_err("with() key must be a string or identifier")),
10364                    };
10365                    let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
10366                    result_df = result_df
10367                        .with_column(&col_name, df_expr)
10368                        .map_err(|e| runtime_err(format!("{e}")))?;
10369                }
10370                Ok(VmValue::Table(VmTable { df: result_df }))
10371            }
10372            "aggregate" => {
10373                let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10374                let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10375                for arg in &ast_args {
10376                    match arg {
10377                        AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
10378                            AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
10379                            AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
10380                            AstExpr::List(items) => {
10381                                for item in items {
10382                                    match item {
10383                                        AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
10384                                        AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
10385                                        _ => {
10386                                            return Err(runtime_err(
10387                                                "by: list items must be strings or identifiers",
10388                                            ));
10389                                        }
10390                                    }
10391                                }
10392                            }
10393                            _ => return Err(runtime_err("by: must be a column name or list")),
10394                        },
10395                        AstExpr::NamedArg { name, value } => {
10396                            let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10397                            agg_exprs.push(agg_expr.alias(name));
10398                        }
10399                        other => {
10400                            let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10401                            agg_exprs.push(agg_expr);
10402                        }
10403                    }
10404                }
10405                let aggregated = df
10406                    .aggregate(group_by_cols, agg_exprs)
10407                    .map_err(|e| runtime_err(format!("{e}")))?;
10408                Ok(VmValue::Table(VmTable { df: aggregated }))
10409            }
10410            "join" => {
10411                if ast_args.is_empty() {
10412                    return Err(runtime_err(
10413                        "join() expects at least 1 argument (right table)",
10414                    ));
10415                }
10416                // Evaluate first arg to get right table
10417                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10418                let right_df = match right_table {
10419                    VmValue::Table(t) => t.df,
10420                    _ => return Err(runtime_err("join() first arg must be a table")),
10421                };
10422                let mut left_cols: Vec<String> = Vec::new();
10423                let mut right_cols: Vec<String> = Vec::new();
10424                let mut join_type = JoinType::Inner;
10425                for arg in &ast_args[1..] {
10426                    match arg {
10427                        AstExpr::NamedArg { name, value } if name == "on" => {
10428                            if let AstExpr::BinOp {
10429                                left,
10430                                op: tl_ast::BinOp::Eq,
10431                                right,
10432                            } = value.as_ref()
10433                            {
10434                                let lc = match left.as_ref() {
10435                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10436                                    _ => {
10437                                        return Err(runtime_err(
10438                                            "on: left side must be a column name",
10439                                        ));
10440                                    }
10441                                };
10442                                let rc = match right.as_ref() {
10443                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10444                                    _ => {
10445                                        return Err(runtime_err(
10446                                            "on: right side must be a column name",
10447                                        ));
10448                                    }
10449                                };
10450                                left_cols.push(lc);
10451                                right_cols.push(rc);
10452                            }
10453                        }
10454                        AstExpr::NamedArg { name, value } if name == "kind" => {
10455                            if let AstExpr::String(kind_str) = value.as_ref() {
10456                                join_type = match kind_str.as_str() {
10457                                    "inner" => JoinType::Inner,
10458                                    "left" => JoinType::Left,
10459                                    "right" => JoinType::Right,
10460                                    "full" => JoinType::Full,
10461                                    _ => {
10462                                        return Err(runtime_err(format!(
10463                                            "Unknown join type: {kind_str}"
10464                                        )));
10465                                    }
10466                                };
10467                            }
10468                        }
10469                        _ => {}
10470                    }
10471                }
10472                let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
10473                let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
10474                let joined = df
10475                    .join(right_df, join_type, &lc_refs, &rc_refs, None)
10476                    .map_err(|e| runtime_err(format!("{e}")))?;
10477                Ok(VmValue::Table(VmTable { df: joined }))
10478            }
10479            "head" | "limit" => {
10480                let n = match ast_args.first() {
10481                    Some(AstExpr::Int(n)) => *n as usize,
10482                    None => 10,
10483                    _ => return Err(runtime_err("head/limit expects an integer")),
10484                };
10485                let limited = df
10486                    .limit(0, Some(n))
10487                    .map_err(|e| runtime_err(format!("{e}")))?;
10488                Ok(VmValue::Table(VmTable { df: limited }))
10489            }
10490            "collect" => {
10491                let batches = self.engine().collect(df).map_err(runtime_err)?;
10492                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10493                Ok(VmValue::String(Arc::from(formatted.as_str())))
10494            }
10495            "show" => {
10496                let limit = match ast_args.first() {
10497                    Some(AstExpr::Int(n)) => *n as usize,
10498                    None => 20,
10499                    _ => 20,
10500                };
10501                let limited = df
10502                    .limit(0, Some(limit))
10503                    .map_err(|e| runtime_err(format!("{e}")))?;
10504                let batches = self.engine().collect(limited).map_err(runtime_err)?;
10505                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10506                println!("{formatted}");
10507                self.output.push(formatted);
10508                Ok(VmValue::None)
10509            }
10510            "describe" => {
10511                let schema = df.schema();
10512                let mut lines = Vec::new();
10513                lines.push("Columns:".to_string());
10514                for field in schema.fields() {
10515                    lines.push(format!("  {}: {}", field.name(), field.data_type()));
10516                }
10517                let output = lines.join("\n");
10518                println!("{output}");
10519                self.output.push(output.clone());
10520                Ok(VmValue::String(Arc::from(output.as_str())))
10521            }
10522            "write_csv" => {
10523                if ast_args.len() != 1 {
10524                    return Err(runtime_err("write_csv() expects 1 argument (path)"));
10525                }
10526                let path = self.eval_ast_to_string(&ast_args[0])?;
10527                self.engine().write_csv(df, &path).map_err(runtime_err)?;
10528                Ok(VmValue::None)
10529            }
10530            "write_parquet" => {
10531                if ast_args.len() != 1 {
10532                    return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10533                }
10534                let path = self.eval_ast_to_string(&ast_args[0])?;
10535                self.engine()
10536                    .write_parquet(df, &path)
10537                    .map_err(runtime_err)?;
10538                Ok(VmValue::None)
10539            }
10540            // Phase 15: Data quality pipe operations
10541            "fill_null" => {
10542                if ast_args.is_empty() {
10543                    return Err(runtime_err(
10544                        "fill_null() expects (column, [strategy/value])",
10545                    ));
10546                }
10547                let column = self.eval_ast_to_string(&ast_args[0])?;
10548                if ast_args.len() >= 2 {
10549                    let val = self.eval_ast_to_vm(&ast_args[1])?;
10550                    match val {
10551                        VmValue::String(s) => {
10552                            // String means strategy name
10553                            let fill_val = if ast_args.len() >= 3 {
10554                                match self.eval_ast_to_vm(&ast_args[2])? {
10555                                    VmValue::Int(n) => Some(n as f64),
10556                                    VmValue::Float(f) => Some(f),
10557                                    _ => None,
10558                                }
10559                            } else {
10560                                None
10561                            };
10562                            let result = self
10563                                .engine()
10564                                .fill_null(df, &column, &s, fill_val)
10565                                .map_err(runtime_err)?;
10566                            Ok(VmValue::Table(VmTable { df: result }))
10567                        }
10568                        VmValue::Int(n) => {
10569                            let result = self
10570                                .engine()
10571                                .fill_null(df, &column, "value", Some(n as f64))
10572                                .map_err(runtime_err)?;
10573                            Ok(VmValue::Table(VmTable { df: result }))
10574                        }
10575                        VmValue::Float(f) => {
10576                            let result = self
10577                                .engine()
10578                                .fill_null(df, &column, "value", Some(f))
10579                                .map_err(runtime_err)?;
10580                            Ok(VmValue::Table(VmTable { df: result }))
10581                        }
10582                        _ => Err(runtime_err(
10583                            "fill_null() second arg must be a strategy or fill value",
10584                        )),
10585                    }
10586                } else {
10587                    let result = self
10588                        .engine()
10589                        .fill_null(df, &column, "zero", None)
10590                        .map_err(runtime_err)?;
10591                    Ok(VmValue::Table(VmTable { df: result }))
10592                }
10593            }
10594            "drop_null" => {
10595                if ast_args.is_empty() {
10596                    return Err(runtime_err("drop_null() expects (column)"));
10597                }
10598                let column = self.eval_ast_to_string(&ast_args[0])?;
10599                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10600                Ok(VmValue::Table(VmTable { df: result }))
10601            }
10602            "dedup" => {
10603                let columns: Vec<String> = ast_args
10604                    .iter()
10605                    .filter_map(|a| self.eval_ast_to_string(a).ok())
10606                    .collect();
10607                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10608                Ok(VmValue::Table(VmTable { df: result }))
10609            }
10610            "clamp" => {
10611                if ast_args.len() < 3 {
10612                    return Err(runtime_err("clamp() expects (column, min, max)"));
10613                }
10614                let column = self.eval_ast_to_string(&ast_args[0])?;
10615                let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10616                    VmValue::Int(n) => n as f64,
10617                    VmValue::Float(f) => f,
10618                    _ => return Err(runtime_err("clamp() min must be a number")),
10619                };
10620                let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10621                    VmValue::Int(n) => n as f64,
10622                    VmValue::Float(f) => f,
10623                    _ => return Err(runtime_err("clamp() max must be a number")),
10624                };
10625                let result = self
10626                    .engine()
10627                    .clamp(df, &column, min_val, max_val)
10628                    .map_err(runtime_err)?;
10629                Ok(VmValue::Table(VmTable { df: result }))
10630            }
10631            "data_profile" => {
10632                let result = self.engine().data_profile(df).map_err(runtime_err)?;
10633                Ok(VmValue::Table(VmTable { df: result }))
10634            }
10635            "row_count" => {
10636                let count = self.engine().row_count(df).map_err(runtime_err)?;
10637                Ok(VmValue::Int(count))
10638            }
10639            "null_rate" => {
10640                if ast_args.is_empty() {
10641                    return Err(runtime_err("null_rate() expects (column)"));
10642                }
10643                let column = self.eval_ast_to_string(&ast_args[0])?;
10644                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10645                Ok(VmValue::Float(rate))
10646            }
10647            "is_unique" => {
10648                if ast_args.is_empty() {
10649                    return Err(runtime_err("is_unique() expects (column)"));
10650                }
10651                let column = self.eval_ast_to_string(&ast_args[0])?;
10652                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10653                Ok(VmValue::Bool(unique))
10654            }
10655            // Phase F2: Window functions
10656            "window" => {
10657                use tl_data::datafusion::logical_expr::{
10658                    WindowFrame, WindowFunctionDefinition,
10659                    expr::{Sort as DfSort, WindowFunction as WinFunc},
10660                };
10661                if ast_args.is_empty() {
10662                    return Err(runtime_err(
10663                        "window() expects named arguments: fn, partition_by, order_by, alias",
10664                    ));
10665                }
10666                let mut win_fn_name = String::new();
10667                let mut partition_by_cols: Vec<String> = Vec::new();
10668                let mut order_by_cols: Vec<String> = Vec::new();
10669                let mut alias_name = String::new();
10670                let mut win_args: Vec<String> = Vec::new();
10671                let mut descending = false;
10672
10673                for arg in &ast_args {
10674                    if let AstExpr::NamedArg { name, value } = arg {
10675                        match name.as_str() {
10676                            "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10677                            "partition_by" => match value.as_ref() {
10678                                AstExpr::List(items) => {
10679                                    for item in items {
10680                                        partition_by_cols.push(self.eval_ast_to_string(item)?);
10681                                    }
10682                                }
10683                                _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10684                            },
10685                            "order_by" => match value.as_ref() {
10686                                AstExpr::List(items) => {
10687                                    for item in items {
10688                                        order_by_cols.push(self.eval_ast_to_string(item)?);
10689                                    }
10690                                }
10691                                _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10692                            },
10693                            "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10694                            "args" => match value.as_ref() {
10695                                AstExpr::List(items) => {
10696                                    for item in items {
10697                                        win_args.push(self.eval_ast_to_string(item)?);
10698                                    }
10699                                }
10700                                _ => win_args.push(self.eval_ast_to_string(value)?),
10701                            },
10702                            "desc" => {
10703                                if let AstExpr::Bool(b) = value.as_ref() {
10704                                    descending = *b;
10705                                }
10706                            }
10707                            _ => {}
10708                        }
10709                    }
10710                }
10711
10712                if win_fn_name.is_empty() {
10713                    return Err(runtime_err(
10714                        "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10715                    ));
10716                }
10717                if alias_name.is_empty() {
10718                    alias_name = win_fn_name.clone();
10719                }
10720
10721                // Build window function definition
10722                let session = self.engine().session_ctx();
10723                let win_udf = match win_fn_name.as_str() {
10724                    "rank" => session.udwf("rank"),
10725                    "dense_rank" => session.udwf("dense_rank"),
10726                    "row_number" => session.udwf("row_number"),
10727                    "percent_rank" => session.udwf("percent_rank"),
10728                    "cume_dist" => session.udwf("cume_dist"),
10729                    "ntile" => session.udwf("ntile"),
10730                    "lag" => session.udwf("lag"),
10731                    "lead" => session.udwf("lead"),
10732                    "first_value" => session.udwf("first_value"),
10733                    "last_value" => session.udwf("last_value"),
10734                    _ => {
10735                        return Err(runtime_err(format!(
10736                            "Unknown window function: {win_fn_name}"
10737                        )));
10738                    }
10739                }
10740                .map_err(|e| {
10741                    runtime_err(format!(
10742                        "Window function '{win_fn_name}' not available: {e}"
10743                    ))
10744                })?;
10745
10746                let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10747
10748                // Build function args (for lag/lead/ntile)
10749                let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10750                    .iter()
10751                    .map(|a| {
10752                        if let Ok(n) = a.parse::<i64>() {
10753                            lit(n)
10754                        } else {
10755                            col(a.as_str())
10756                        }
10757                    })
10758                    .collect();
10759
10760                let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10761                    partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10762                let order_exprs: Vec<DfSort> = order_by_cols
10763                    .iter()
10764                    .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10765                    .collect();
10766
10767                let has_order = !order_exprs.is_empty();
10768                let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10769                    fun,
10770                    args: func_args,
10771                    partition_by: partition_exprs,
10772                    order_by: order_exprs,
10773                    window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10774                    null_treatment: None,
10775                })
10776                .alias(&alias_name);
10777
10778                // Get all existing columns and add the window column
10779                let schema = df.schema();
10780                let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10781                    .fields()
10782                    .iter()
10783                    .map(|f| col(f.name().as_str()))
10784                    .collect();
10785                select_exprs.push(win_expr);
10786
10787                let result_df = df
10788                    .select(select_exprs)
10789                    .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10790                Ok(VmValue::Table(VmTable { df: result_df }))
10791            }
10792            // Phase F3: Union
10793            "union" => {
10794                if ast_args.is_empty() {
10795                    return Err(runtime_err("union() expects a table argument"));
10796                }
10797                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10798                let right_df = match right_table {
10799                    VmValue::Table(t) => t.df,
10800                    _ => return Err(runtime_err("union() argument must be a table")),
10801                };
10802                let result_df = df
10803                    .union(right_df)
10804                    .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10805                Ok(VmValue::Table(VmTable { df: result_df }))
10806            }
10807            // Phase F4: Table sampling
10808            "sample" => {
10809                use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10810                use tl_data::datafusion::datasource::MemTable;
10811                if ast_args.is_empty() {
10812                    return Err(runtime_err("sample() expects a count or fraction"));
10813                }
10814                let batches = self.engine().collect(df).map_err(runtime_err)?;
10815                let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10816                let sample_count = match &ast_args[0] {
10817                    AstExpr::Int(n) => (*n as usize).min(total_rows),
10818                    AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10819                        ((total_rows as f64) * f).ceil() as usize
10820                    }
10821                    _ => {
10822                        let val = self.eval_ast_to_string(&ast_args[0])?;
10823                        val.parse::<usize>().map_err(|_| {
10824                            runtime_err("sample() expects integer count or float fraction")
10825                        })?
10826                    }
10827                };
10828                if total_rows == 0 || sample_count == 0 {
10829                    let schema = batches[0].schema();
10830                    let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10831                        schema.clone(),
10832                    );
10833                    let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10834                        .map_err(|e| runtime_err(format!("{e}")))?;
10835                    let new_df = self
10836                        .engine()
10837                        .session_ctx()
10838                        .read_table(Arc::new(mem_table))
10839                        .map_err(|e| runtime_err(format!("{e}")))?;
10840                    return Ok(VmValue::Table(VmTable { df: new_df }));
10841                }
10842                // Random sampling
10843                let mut rng = rand::thread_rng();
10844                let mut indices: Vec<usize> = (0..total_rows).collect();
10845                use rand::seq::SliceRandom;
10846                indices.partial_shuffle(&mut rng, sample_count);
10847                indices.truncate(sample_count);
10848                indices.sort();
10849                // Concatenate and take
10850                let combined = compute::concat_batches(&batches[0].schema(), &batches)
10851                    .map_err(|e| runtime_err(format!("{e}")))?;
10852                let idx_array =
10853                    UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10854                let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10855                    .num_columns())
10856                    .map(|c| {
10857                        compute::take(combined.column(c), &idx_array, None)
10858                            .map_err(|e| runtime_err(format!("{e}")))
10859                    })
10860                    .collect::<Result<Vec<_>, _>>()?;
10861                let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10862                    combined.schema(),
10863                    sampled_cols,
10864                )
10865                .map_err(|e| runtime_err(format!("{e}")))?;
10866                let mem_table =
10867                    MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10868                        .map_err(|e| runtime_err(format!("{e}")))?;
10869                let new_df = self
10870                    .engine()
10871                    .session_ctx()
10872                    .read_table(Arc::new(mem_table))
10873                    .map_err(|e| runtime_err(format!("{e}")))?;
10874                Ok(VmValue::Table(VmTable { df: new_df }))
10875            }
10876            _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10877        }
10878    }
10879
10880    /// Fallback for table pipe when left side is not a table.
10881    /// Converts to a regular function/builtin call with left as first arg.
10882    fn table_pipe_fallback(
10883        &mut self,
10884        left_val: VmValue,
10885        frame_idx: usize,
10886        op_const: u8,
10887        args_const: u8,
10888    ) -> Result<VmValue, TlError> {
10889        let frame = &self.frames[frame_idx];
10890        let op_name = match &frame.prototype.constants[op_const as usize] {
10891            Constant::String(s) => s.to_string(),
10892            _ => return Err(runtime_err("Expected string constant for table op")),
10893        };
10894        let ast_args = match &frame.prototype.constants[args_const as usize] {
10895            Constant::AstExprList(args) => args.clone(),
10896            _ => return Err(runtime_err("Expected AST expr list for table args")),
10897        };
10898
10899        // Try as builtin with left as first arg
10900        if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10901            // Evaluate AST args to VM values
10902            let mut all_args = vec![left_val];
10903            for arg in &ast_args {
10904                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10905            }
10906            let args_base = self.stack.len();
10907            for arg in &all_args {
10908                self.stack.push(arg.clone());
10909            }
10910            let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10911            self.stack.truncate(args_base);
10912            return result;
10913        }
10914
10915        // Try as user-defined function
10916        if let Some(func) = self.globals.get(&op_name).cloned() {
10917            let mut all_args = vec![left_val];
10918            for arg in &ast_args {
10919                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10920            }
10921            return self.call_vm_function(&func, &all_args);
10922        }
10923
10924        Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10925    }
10926
10927    /// Build TranslateContext from VM globals and stack.
10928    #[cfg(feature = "native")]
10929    fn build_translate_context(&self) -> TranslateContext {
10930        let mut ctx = TranslateContext::new();
10931        // Add globals
10932        for (name, val) in &self.globals {
10933            let local = match val {
10934                VmValue::Int(n) => Some(LocalValue::Int(*n)),
10935                VmValue::Float(f) => Some(LocalValue::Float(*f)),
10936                VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10937                VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10938                _ => None,
10939            };
10940            if let Some(l) = local {
10941                ctx.locals.insert(name.clone(), l);
10942            }
10943        }
10944        // Add locals from current frame
10945        if let Some(frame) = self.frames.last() {
10946            for local_idx in 0..frame.prototype.num_locals as usize {
10947                if let Some(val) = self.stack.get(frame.base + local_idx) {
10948                    // We'd need local name info — for now, rely on globals
10949                    let _ = val;
10950                }
10951            }
10952        }
10953        ctx
10954    }
10955
10956    /// Evaluate an AST expression to a VmValue.
10957    /// For simple expressions does direct lookup; for complex ones, compiles and runs.
10958    fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
10959        match expr {
10960            AstExpr::Ident(name) => {
10961                // Look up in globals first
10962                if let Some(val) = self.globals.get(name) {
10963                    return Ok(val.clone());
10964                }
10965                // Check current frame's stack
10966                if let Some(frame) = self.frames.last() {
10967                    for i in 0..frame.prototype.num_registers as usize {
10968                        if let Some(val) = self.stack.get(frame.base + i)
10969                            && !matches!(val, VmValue::None)
10970                        {
10971                            // Without name->register mapping, we can't be sure
10972                            // which register holds this variable
10973                        }
10974                    }
10975                }
10976                Err(runtime_err(format!("Undefined variable: `{name}`")))
10977            }
10978            AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
10979            AstExpr::Int(n) => Ok(VmValue::Int(*n)),
10980            AstExpr::Float(f) => Ok(VmValue::Float(*f)),
10981            AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
10982            AstExpr::None => Ok(VmValue::None),
10983            AstExpr::Closure {
10984                params: _, body: _, ..
10985            } => {
10986                use crate::compiler;
10987                let wrapper = tl_ast::Program {
10988                    statements: vec![tl_ast::Stmt {
10989                        kind: tl_ast::StmtKind::Expr(expr.clone()),
10990                        span: tl_errors::Span::new(0, 0),
10991                        doc_comment: None,
10992                    }],
10993                    module_doc: None,
10994                };
10995                let proto = compiler::compile(&wrapper)?;
10996                let mut temp_vm = Vm::new();
10997                // Copy globals
10998                temp_vm.globals = self.globals.clone();
10999                let result = temp_vm.execute(&proto)?;
11000                Ok(result)
11001            }
11002            _ => {
11003                // For complex expressions, compile and evaluate
11004                let wrapper = tl_ast::Program {
11005                    statements: vec![tl_ast::Stmt {
11006                        kind: tl_ast::StmtKind::Expr(expr.clone()),
11007                        span: tl_errors::Span::new(0, 0),
11008                        doc_comment: None,
11009                    }],
11010                    module_doc: None,
11011                };
11012                use crate::compiler;
11013                let proto = compiler::compile(&wrapper)?;
11014                let mut temp_vm = Vm::new();
11015                temp_vm.globals = self.globals.clone();
11016                temp_vm.execute(&proto)
11017            }
11018        }
11019    }
11020
11021    fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
11022        match self.eval_ast_to_vm(expr)? {
11023            VmValue::String(s) => Ok(s.to_string()),
11024            _ => Err(runtime_err("Expected a string")),
11025        }
11026    }
11027
11028    /// Simple string interpolation.
11029    fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
11030        let mut result = String::new();
11031        let mut chars = s.chars().peekable();
11032        while let Some(ch) = chars.next() {
11033            if ch == '{' {
11034                let mut var_name = String::new();
11035                let mut depth = 1;
11036                for c in chars.by_ref() {
11037                    if c == '{' {
11038                        depth += 1;
11039                    } else if c == '}' {
11040                        depth -= 1;
11041                        if depth == 0 {
11042                            break;
11043                        }
11044                    }
11045                    var_name.push(c);
11046                }
11047                // Look up variable
11048                if let Some(val) = self.globals.get(&var_name) {
11049                    result.push_str(&format!("{val}"));
11050                } else {
11051                    // Check locals in current frame
11052                    // For now, fall back to globals only — local name info
11053                    // would need debug symbols from the compiler
11054                    result.push('{');
11055                    result.push_str(&var_name);
11056                    result.push('}');
11057                }
11058            } else if ch == '\\' {
11059                match chars.next() {
11060                    Some('n') => result.push('\n'),
11061                    Some('t') => result.push('\t'),
11062                    Some('\\') => result.push('\\'),
11063                    Some('"') => result.push('"'),
11064                    Some(c) => {
11065                        result.push('\\');
11066                        result.push(c);
11067                    }
11068                    None => result.push('\\'),
11069                }
11070            } else {
11071                result.push(ch);
11072            }
11073        }
11074        Ok(result)
11075    }
11076
11077    /// Execute a single bytecode instruction at the given base offset.
11078    /// Used by the LLVM backend's Tier 3 fallback to run complex opcodes on the VM.
11079    pub fn execute_single_instruction(
11080        &mut self,
11081        inst: u32,
11082        proto: &Prototype,
11083        base: usize,
11084    ) -> Result<Option<VmValue>, TlError> {
11085        use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
11086
11087        let proto = Arc::new(proto.clone());
11088        // Push a temporary call frame so the VM can resolve constants etc.
11089        self.frames.push(CallFrame {
11090            prototype: proto.clone(),
11091            ip: 0,
11092            base,
11093            upvalues: Vec::new(),
11094        });
11095        let frame_idx = self.frames.len() - 1;
11096
11097        let op = decode_op(inst);
11098        let a = decode_a(inst);
11099        let _b = decode_b(inst);
11100        let _c = decode_c(inst);
11101        let bx = decode_bx(inst);
11102
11103        // Dispatch the single opcode. We handle the most common
11104        // Tier 3 ops here; anything not handled returns Ok(None).
11105        let result = match op {
11106            Op::GetGlobal => {
11107                let name = self.get_string_constant(frame_idx, bx)?;
11108                let val = self
11109                    .globals
11110                    .get(name.as_ref())
11111                    .cloned()
11112                    .unwrap_or(VmValue::None);
11113                self.stack[base + a as usize] = val;
11114                Ok(None)
11115            }
11116            Op::SetGlobal => {
11117                let name = self.get_string_constant(frame_idx, bx)?;
11118                let val = self.stack[base + a as usize].clone();
11119                self.globals.insert(name.to_string(), val);
11120                Ok(None)
11121            }
11122            _ => {
11123                // For opcodes not explicitly handled, return Ok — the caller
11124                // should have handled Tier 1/2 in LLVM IR.
11125                Ok(None)
11126            }
11127        };
11128
11129        self.frames.pop();
11130        result
11131    }
11132}
11133
11134impl Default for Vm {
11135    fn default() -> Self {
11136        Self::new()
11137    }
11138}
11139
11140#[cfg(test)]
11141mod tests {
11142    use super::*;
11143    use crate::compiler::compile;
11144    use tl_parser::parse;
11145
11146    fn run(source: &str) -> Result<VmValue, TlError> {
11147        let program = parse(source)?;
11148        let proto = compile(&program)?;
11149        let mut vm = Vm::new();
11150        vm.execute(&proto)
11151    }
11152
11153    fn run_output(source: &str) -> Vec<String> {
11154        let program = parse(source).unwrap();
11155        let proto = compile(&program).unwrap();
11156        let mut vm = Vm::new();
11157        vm.execute(&proto).unwrap();
11158        vm.output
11159    }
11160
11161    #[test]
11162    fn test_vm_arithmetic() {
11163        assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
11164        assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
11165        assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
11166        assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
11167        assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
11168        assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
11169        let output = run_output("print(1 + 2)");
11170        assert_eq!(output, vec!["3"]);
11171    }
11172
11173    #[test]
11174    fn test_vm_let_and_print() {
11175        let output = run_output("let x = 42\nprint(x)");
11176        assert_eq!(output, vec!["42"]);
11177    }
11178
11179    #[test]
11180    fn test_vm_function() {
11181        let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
11182        assert_eq!(output, vec!["42"]);
11183    }
11184
11185    #[test]
11186    fn test_vm_if_else() {
11187        let output =
11188            run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
11189        assert_eq!(output, vec!["big"]);
11190    }
11191
11192    #[test]
11193    fn test_vm_list() {
11194        let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
11195        assert_eq!(output, vec!["3"]);
11196    }
11197
11198    #[test]
11199    fn test_vm_map_builtin() {
11200        let output = run_output(
11201            "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
11202        );
11203        assert_eq!(output, vec!["[2, 4, 6]"]);
11204    }
11205
11206    #[test]
11207    fn test_vm_filter_builtin() {
11208        let output = run_output(
11209            "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
11210        );
11211        assert_eq!(output, vec!["[2, 4]"]);
11212    }
11213
11214    #[test]
11215    fn test_vm_for_loop() {
11216        let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
11217        assert_eq!(output, vec!["10"]);
11218    }
11219
11220    #[test]
11221    fn test_vm_closure() {
11222        let output = run_output("let double = (x) => x * 2\nprint(double(5))");
11223        assert_eq!(output, vec!["10"]);
11224    }
11225
11226    #[test]
11227    fn test_vm_sum() {
11228        let output = run_output("print(sum([1, 2, 3, 4]))");
11229        assert_eq!(output, vec!["10"]);
11230    }
11231
11232    #[test]
11233    fn test_vm_reduce() {
11234        let output = run_output(
11235            "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
11236        );
11237        assert_eq!(output, vec!["24"]);
11238    }
11239
11240    #[test]
11241    fn test_vm_pipe() {
11242        let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
11243        assert_eq!(output, vec!["[11, 12, 13]"]);
11244    }
11245
11246    #[test]
11247    fn test_vm_comparison() {
11248        let output = run_output("print(5 > 3)");
11249        assert_eq!(output, vec!["true"]);
11250    }
11251
11252    #[test]
11253    fn test_vm_precedence() {
11254        let output = run_output("print(2 + 3 * 4)");
11255        assert_eq!(output, vec!["14"]);
11256    }
11257
11258    #[test]
11259    fn test_vm_match() {
11260        let output =
11261            run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
11262        assert_eq!(output, vec!["two"]);
11263    }
11264
11265    #[test]
11266    fn test_vm_match_wildcard() {
11267        let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
11268        assert_eq!(output, vec!["other"]);
11269    }
11270
11271    #[test]
11272    fn test_vm_match_binding() {
11273        let output = run_output("print(match 42 { val => val + 1 })");
11274        assert_eq!(output, vec!["43"]);
11275    }
11276
11277    #[test]
11278    fn test_vm_match_guard() {
11279        let output = run_output(
11280            "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11281        );
11282        assert_eq!(output, vec!["pos"]);
11283    }
11284
11285    #[test]
11286    fn test_vm_match_guard_negative() {
11287        let output = run_output(
11288            "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11289        );
11290        assert_eq!(output, vec!["neg"]);
11291    }
11292
11293    #[test]
11294    fn test_vm_match_guard_zero() {
11295        let output = run_output(
11296            "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11297        );
11298        assert_eq!(output, vec!["zero"]);
11299    }
11300
11301    #[test]
11302    fn test_vm_match_enum_destructure() {
11303        let output = run_output(
11304            r#"
11305enum Shape { Circle(int64), Rect(int64, int64) }
11306let s = Shape::Circle(5)
11307print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11308"#,
11309        );
11310        assert_eq!(output, vec!["5"]);
11311    }
11312
11313    #[test]
11314    fn test_vm_match_enum_destructure_rect() {
11315        let output = run_output(
11316            r#"
11317enum Shape { Circle(int64), Rect(int64, int64) }
11318let s = Shape::Rect(3, 4)
11319print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11320"#,
11321        );
11322        assert_eq!(output, vec!["12"]);
11323    }
11324
11325    #[test]
11326    fn test_vm_match_enum_wildcard_field() {
11327        let output = run_output(
11328            r#"
11329enum Pair { Two(int64, int64) }
11330let p = Pair::Two(10, 20)
11331print(match p { Pair::Two(_, y) => y, _ => 0 })
11332"#,
11333        );
11334        assert_eq!(output, vec!["20"]);
11335    }
11336
11337    #[test]
11338    fn test_vm_match_enum_guard() {
11339        let output = run_output(
11340            r#"
11341enum Result { Ok(int64), Err(string) }
11342let r = Result::Ok(150)
11343print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
11344"#,
11345        );
11346        assert_eq!(output, vec!["big"]);
11347    }
11348
11349    #[test]
11350    fn test_vm_match_or_pattern() {
11351        let output =
11352            run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11353        assert_eq!(output, vec!["small"]);
11354    }
11355
11356    #[test]
11357    fn test_vm_match_or_pattern_no_match() {
11358        let output =
11359            run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11360        assert_eq!(output, vec!["big"]);
11361    }
11362
11363    #[test]
11364    fn test_vm_match_string() {
11365        let output = run_output(
11366            r#"let s = "hello"
11367print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
11368        );
11369        assert_eq!(output, vec!["2"]);
11370    }
11371
11372    #[test]
11373    fn test_vm_match_bool() {
11374        let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
11375        assert_eq!(output, vec!["yes"]);
11376    }
11377
11378    #[test]
11379    fn test_vm_match_none() {
11380        let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
11381        assert_eq!(output, vec!["nothing"]);
11382    }
11383
11384    #[test]
11385    fn test_vm_let_destructure_list() {
11386        let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
11387        assert_eq!(output, vec!["1", "2", "3"]);
11388    }
11389
11390    #[test]
11391    fn test_vm_let_destructure_list_rest() {
11392        let output =
11393            run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
11394        assert_eq!(output, vec!["1", "3"]);
11395    }
11396
11397    #[test]
11398    fn test_vm_let_destructure_struct() {
11399        let output = run_output(
11400            r#"
11401struct Point { x: int64, y: int64 }
11402let p = Point { x: 10, y: 20 }
11403let Point { x, y } = p
11404print(x)
11405print(y)
11406"#,
11407        );
11408        assert_eq!(output, vec!["10", "20"]);
11409    }
11410
11411    #[test]
11412    fn test_vm_let_destructure_struct_anon() {
11413        let output = run_output(
11414            r#"
11415struct Point { x: int64, y: int64 }
11416let p = Point { x: 10, y: 20 }
11417let { x, y } = p
11418print(x)
11419print(y)
11420"#,
11421        );
11422        assert_eq!(output, vec!["10", "20"]);
11423    }
11424
11425    #[test]
11426    fn test_vm_match_struct_pattern() {
11427        let output = run_output(
11428            r#"
11429struct Point { x: int64, y: int64 }
11430let p = Point { x: 1, y: 2 }
11431print(match p { Point { x, y } => x + y, _ => 0 })
11432"#,
11433        );
11434        assert_eq!(output, vec!["3"]);
11435    }
11436
11437    #[test]
11438    fn test_vm_match_list_pattern() {
11439        let output = run_output(
11440            r#"
11441let lst = [1, 2, 3]
11442print(match lst { [a, b, c] => a + b + c, _ => 0 })
11443"#,
11444        );
11445        assert_eq!(output, vec!["6"]);
11446    }
11447
11448    #[test]
11449    fn test_vm_match_list_rest_pattern() {
11450        let output = run_output(
11451            r#"
11452let lst = [10, 20, 30, 40]
11453print(match lst { [head, ...rest] => head, _ => 0 })
11454"#,
11455        );
11456        assert_eq!(output, vec!["10"]);
11457    }
11458
11459    #[test]
11460    fn test_vm_match_list_empty() {
11461        let output = run_output(
11462            r#"
11463let lst = []
11464print(match lst { [] => "empty", _ => "nonempty" })
11465"#,
11466        );
11467        assert_eq!(output, vec!["empty"]);
11468    }
11469
11470    #[test]
11471    fn test_vm_match_list_length_mismatch() {
11472        let output = run_output(
11473            r#"
11474let lst = [1, 2, 3]
11475print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
11476"#,
11477        );
11478        assert_eq!(output, vec!["three"]);
11479    }
11480
11481    #[test]
11482    fn test_vm_match_negative_literal() {
11483        let output =
11484            run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
11485        assert_eq!(output, vec!["neg one"]);
11486    }
11487
11488    #[test]
11489    fn test_vm_case_with_pattern() {
11490        let output = run_output(
11491            r#"
11492let x = 5
11493let result = case {
11494    x > 10 => "big",
11495    x > 0 => "positive",
11496    _ => "other"
11497}
11498print(result)
11499"#,
11500        );
11501        assert_eq!(output, vec!["positive"]);
11502    }
11503
11504    #[test]
11505    fn test_vm_parallel_map() {
11506        // Build a range > PARALLEL_THRESHOLD and map with a pure function
11507        let result = run("map(range(15000), (x) => x * 2)").unwrap();
11508        if let VmValue::List(items) = result {
11509            assert_eq!(items.len(), 15000);
11510            assert!(matches!(items[0], VmValue::Int(0)));
11511            assert!(matches!(items[1], VmValue::Int(2)));
11512            assert!(matches!(items[14999], VmValue::Int(29998)));
11513        } else {
11514            panic!("Expected list, got {:?}", result);
11515        }
11516    }
11517
11518    #[test]
11519    fn test_vm_parallel_filter() {
11520        let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11521        if let VmValue::List(items) = result {
11522            assert_eq!(items.len(), 10000);
11523            assert!(matches!(items[0], VmValue::Int(0)));
11524            assert!(matches!(items[1], VmValue::Int(2)));
11525        } else {
11526            panic!("Expected list, got {:?}", result);
11527        }
11528    }
11529
11530    #[test]
11531    fn test_vm_parallel_sum() {
11532        let result = run("sum(range(20000))").unwrap();
11533        // sum(0..19999) = 19999 * 20000 / 2 = 199990000
11534        assert!(matches!(result, VmValue::Int(199990000)));
11535    }
11536
11537    #[test]
11538    fn test_vm_recursive_fib() {
11539        let output = run_output(
11540            "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11541        );
11542        assert_eq!(output, vec!["55"]);
11543    }
11544
11545    #[test]
11546    fn test_vm_if_else_expr() {
11547        // if-else as the last expression in a function should return a value
11548        let output = run_output(
11549            "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11550        );
11551        assert_eq!(output, vec!["5", "3"]);
11552    }
11553
11554    // ── Phase 5 tests ──
11555
11556    #[test]
11557    fn test_vm_struct_creation() {
11558        let output = run_output(
11559            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11560        );
11561        assert_eq!(output, vec!["1.0", "2.0"]);
11562    }
11563
11564    #[test]
11565    fn test_vm_struct_nested() {
11566        let output = run_output(
11567            "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)",
11568        );
11569        assert_eq!(output, vec!["0.0"]);
11570    }
11571
11572    #[test]
11573    fn test_vm_enum_creation() {
11574        let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11575        assert_eq!(output, vec!["Color::Red"]);
11576    }
11577
11578    #[test]
11579    fn test_vm_enum_with_fields() {
11580        let output = run_output(
11581            "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11582        );
11583        assert!(output[0].contains("Circle"));
11584    }
11585
11586    #[test]
11587    fn test_vm_impl_method() {
11588        let output = run_output(
11589            "struct Counter { value: int64 }\nimpl Counter {\n    fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11590        );
11591        assert_eq!(output, vec!["42"]);
11592    }
11593
11594    #[test]
11595    fn test_vm_try_catch_throw() {
11596        let output = run_output("try {\n    throw \"oops\"\n} catch e {\n    print(e)\n}");
11597        assert_eq!(output, vec!["oops"]);
11598    }
11599
11600    #[test]
11601    fn test_vm_string_split() {
11602        let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11603        assert_eq!(output, vec!["[hello, world]"]);
11604    }
11605
11606    #[test]
11607    fn test_vm_string_trim() {
11608        let output = run_output("print(\"  hello  \".trim())");
11609        assert_eq!(output, vec!["hello"]);
11610    }
11611
11612    #[test]
11613    fn test_vm_string_contains() {
11614        let output = run_output("print(\"hello world\".contains(\"world\"))");
11615        assert_eq!(output, vec!["true"]);
11616    }
11617
11618    #[test]
11619    fn test_vm_string_upper_lower() {
11620        let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11621        assert_eq!(output, vec!["HELLO", "hello"]);
11622    }
11623
11624    #[test]
11625    fn test_vm_math_sqrt() {
11626        let output = run_output("print(sqrt(16.0))");
11627        assert_eq!(output, vec!["4.0"]);
11628    }
11629
11630    #[test]
11631    fn test_vm_math_floor_ceil() {
11632        let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11633        assert_eq!(output, vec!["3.0", "4.0"]);
11634    }
11635
11636    #[test]
11637    fn test_vm_math_trig() {
11638        let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11639        assert_eq!(output, vec!["0.0", "1.0"]);
11640    }
11641
11642    #[test]
11643    fn test_vm_assert_pass() {
11644        run("assert(true)").unwrap();
11645        run("assert_eq(1 + 1, 2)").unwrap();
11646    }
11647
11648    #[test]
11649    fn test_vm_assert_fail() {
11650        assert!(run("assert(false)").is_err());
11651        assert!(run("assert_eq(1, 2)").is_err());
11652    }
11653
11654    #[test]
11655    fn test_vm_join() {
11656        let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11657        assert_eq!(output, vec!["a, b, c"]);
11658    }
11659
11660    #[test]
11661    fn test_vm_list_method_len() {
11662        let output = run_output("print([1, 2, 3].len())");
11663        assert_eq!(output, vec!["3"]);
11664    }
11665
11666    #[test]
11667    fn test_vm_list_method_map() {
11668        let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11669        assert_eq!(output, vec!["[2, 4, 6]"]);
11670    }
11671
11672    #[test]
11673    fn test_vm_list_method_filter() {
11674        let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11675        assert_eq!(output, vec!["[4, 5]"]);
11676    }
11677
11678    #[test]
11679    fn test_vm_string_replace() {
11680        let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11681        assert_eq!(output, vec!["hello rust"]);
11682    }
11683
11684    #[test]
11685    fn test_vm_string_starts_ends() {
11686        let output = run_output(
11687            "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11688        );
11689        assert_eq!(output, vec!["true", "true"]);
11690    }
11691
11692    #[test]
11693    fn test_vm_math_log() {
11694        let result = run("log(1.0)").unwrap();
11695        if let VmValue::Float(f) = result {
11696            assert!((f - 0.0).abs() < 1e-10);
11697        } else {
11698            panic!("Expected float");
11699        }
11700    }
11701
11702    #[test]
11703    fn test_vm_pow_builtin() {
11704        let output = run_output("print(pow(2.0, 10.0))");
11705        assert_eq!(output, vec!["1024.0"]);
11706    }
11707
11708    #[test]
11709    fn test_vm_round_builtin() {
11710        let output = run_output("print(round(3.5))");
11711        assert_eq!(output, vec!["4.0"]);
11712    }
11713
11714    #[test]
11715    fn test_vm_try_catch_runtime_error() {
11716        let output = run_output("try {\n    let x = 1 / 0\n} catch e {\n    print(e)\n}");
11717        assert_eq!(output, vec!["Division by zero"]);
11718    }
11719
11720    #[test]
11721    fn test_vm_struct_field_access() {
11722        let output = run_output(
11723            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11724        );
11725        assert_eq!(output, vec!["1.5"]);
11726    }
11727
11728    #[test]
11729    fn test_vm_enum_match() {
11730        let output = run_output(
11731            "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11732        );
11733        // match expression compares enum instances
11734        assert!(!output.is_empty());
11735    }
11736
11737    #[test]
11738    fn test_vm_impl_method_with_args() {
11739        let output = run_output(
11740            "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())",
11741        );
11742        assert_eq!(output, vec!["12.0"]);
11743    }
11744
11745    #[test]
11746    fn test_vm_string_len() {
11747        let output = run_output("print(\"hello\".len())");
11748        assert_eq!(output, vec!["5"]);
11749    }
11750
11751    #[test]
11752    fn test_vm_list_reduce() {
11753        let output = run_output(
11754            "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11755        );
11756        assert_eq!(output, vec!["10"]);
11757    }
11758
11759    #[test]
11760    fn test_vm_nested_try_catch() {
11761        let output = run_output(
11762            "try {\n    try {\n        throw \"inner\"\n    } catch e {\n        print(e)\n        throw \"outer\"\n    }\n} catch e2 {\n    print(e2)\n}",
11763        );
11764        assert_eq!(output, vec!["inner", "outer"]);
11765    }
11766
11767    #[test]
11768    fn test_vm_math_pow() {
11769        let output = run_output("print(pow(2.0, 10.0))");
11770        assert_eq!(output, vec!["1024.0"]);
11771    }
11772
11773    // ── Phase 6: Stdlib & Ecosystem tests ──
11774
11775    #[test]
11776    fn test_vm_json_parse() {
11777        let output = run_output(
11778            r#"let m = map_from("a", 1, "b", "hello")
11779let s = json_stringify(m)
11780let m2 = json_parse(s)
11781print(m2["a"])
11782print(m2["b"])"#,
11783        );
11784        assert_eq!(output, vec!["1", "hello"]);
11785    }
11786
11787    #[test]
11788    fn test_vm_json_stringify() {
11789        let output = run_output(
11790            r#"let m = map_from("x", 1, "y", 2)
11791let s = json_stringify(m)
11792print(s)"#,
11793        );
11794        assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11795    }
11796
11797    #[test]
11798    fn test_vm_map_from_and_access() {
11799        let output = run_output(
11800            r#"let m = map_from("a", 10, "b", 20)
11801print(m["a"])
11802print(m.b)"#,
11803        );
11804        assert_eq!(output, vec!["10", "20"]);
11805    }
11806
11807    #[test]
11808    fn test_vm_map_methods() {
11809        let output = run_output(
11810            r#"let m = map_from("a", 1, "b", 2)
11811print(m.keys())
11812print(m.values())
11813print(m.contains_key("a"))
11814print(m.len())"#,
11815        );
11816        assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11817    }
11818
11819    #[test]
11820    fn test_vm_map_set_index() {
11821        let output = run_output(
11822            r#"let m = map_from("a", 1)
11823m["b"] = 2
11824print(m["b"])"#,
11825        );
11826        assert_eq!(output, vec!["2"]);
11827    }
11828
11829    #[test]
11830    fn test_vm_map_iteration() {
11831        let output = run_output(
11832            r#"let m = map_from("x", 10, "y", 20)
11833for kv in m {
11834    print(kv[0])
11835}"#,
11836        );
11837        assert_eq!(output, vec!["x", "y"]);
11838    }
11839
11840    #[test]
11841    fn test_vm_file_read_write() {
11842        let output = run_output(
11843            r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11844print(read_file("/tmp/tl_vm_test.txt"))
11845print(file_exists("/tmp/tl_vm_test.txt"))"#,
11846        );
11847        assert_eq!(output, vec!["vm hello", "true"]);
11848    }
11849
11850    #[test]
11851    fn test_vm_env_get_set() {
11852        let output = run_output(
11853            r#"env_set("TL_VM_TEST", "abc")
11854print(env_get("TL_VM_TEST"))"#,
11855        );
11856        assert_eq!(output, vec!["abc"]);
11857    }
11858
11859    #[test]
11860    fn test_vm_regex_match() {
11861        let output = run_output(
11862            r#"print(regex_match("\\d+", "abc123"))
11863print(regex_match("^\\d+$", "abc"))"#,
11864        );
11865        assert_eq!(output, vec!["true", "false"]);
11866    }
11867
11868    #[test]
11869    fn test_vm_regex_find() {
11870        let output = run_output(
11871            r#"let m = regex_find("\\d+", "abc123def456")
11872print(len(m))
11873print(m[0])"#,
11874        );
11875        assert_eq!(output, vec!["2", "123"]);
11876    }
11877
11878    #[test]
11879    fn test_vm_regex_replace() {
11880        let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11881        assert_eq!(output, vec!["abcX"]);
11882    }
11883
11884    #[test]
11885    fn test_vm_now() {
11886        // now() returns DateTime which displays as formatted string
11887        let output = run_output("let t = now()\nprint(type_of(t))");
11888        assert_eq!(output, vec!["datetime"]);
11889    }
11890
11891    #[test]
11892    fn test_vm_date_format() {
11893        let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11894        assert_eq!(output, vec!["2024-01-01"]);
11895    }
11896
11897    #[test]
11898    fn test_vm_date_parse() {
11899        let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11900        assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11901    }
11902
11903    #[test]
11904    fn test_vm_string_chars() {
11905        let output = run_output(r#"print(len("hello".chars()))"#);
11906        assert_eq!(output, vec!["5"]);
11907    }
11908
11909    #[test]
11910    fn test_vm_string_repeat() {
11911        let output = run_output(r#"print("ab".repeat(3))"#);
11912        assert_eq!(output, vec!["ababab"]);
11913    }
11914
11915    #[test]
11916    fn test_vm_string_index_of() {
11917        let output = run_output(r#"print("hello world".index_of("world"))"#);
11918        assert_eq!(output, vec!["6"]);
11919    }
11920
11921    #[test]
11922    fn test_vm_string_substring() {
11923        let output = run_output(r#"print("hello world".substring(0, 5))"#);
11924        assert_eq!(output, vec!["hello"]);
11925    }
11926
11927    #[test]
11928    fn test_vm_string_pad() {
11929        let output = run_output(
11930            r#"print("42".pad_left(5, "0"))
11931print("hi".pad_right(5, "."))"#,
11932        );
11933        assert_eq!(output, vec!["00042", "hi..."]);
11934    }
11935
11936    #[test]
11937    fn test_vm_list_sort() {
11938        let output = run_output(r#"print([3, 1, 2].sort())"#);
11939        assert_eq!(output, vec!["[1, 2, 3]"]);
11940    }
11941
11942    #[test]
11943    fn test_vm_list_reverse() {
11944        let output = run_output(r#"print([1, 2, 3].reverse())"#);
11945        assert_eq!(output, vec!["[3, 2, 1]"]);
11946    }
11947
11948    #[test]
11949    fn test_vm_list_contains() {
11950        let output = run_output(
11951            r#"print([1, 2, 3].contains(2))
11952print([1, 2, 3].contains(5))"#,
11953        );
11954        assert_eq!(output, vec!["true", "false"]);
11955    }
11956
11957    #[test]
11958    fn test_vm_list_slice() {
11959        let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
11960        assert_eq!(output, vec!["[2, 3, 4]"]);
11961    }
11962
11963    #[test]
11964    fn test_vm_zip() {
11965        let output = run_output(
11966            r#"let p = zip([1, 2], ["a", "b"])
11967print(p[0])"#,
11968        );
11969        assert_eq!(output, vec!["[1, a]"]);
11970    }
11971
11972    #[test]
11973    fn test_vm_enumerate() {
11974        let output = run_output(
11975            r#"let e = enumerate(["a", "b", "c"])
11976print(e[1])"#,
11977        );
11978        assert_eq!(output, vec!["[1, b]"]);
11979    }
11980
11981    #[test]
11982    fn test_vm_bool() {
11983        let output = run_output(
11984            r#"print(bool(1))
11985print(bool(0))
11986print(bool(""))"#,
11987        );
11988        assert_eq!(output, vec!["true", "false", "false"]);
11989    }
11990
11991    #[test]
11992    fn test_vm_range_step() {
11993        let output = run_output(r#"print(range(0, 10, 3))"#);
11994        assert_eq!(output, vec!["[0, 3, 6, 9]"]);
11995    }
11996
11997    #[test]
11998    fn test_vm_int_bool() {
11999        let output = run_output(
12000            r#"print(int(true))
12001print(int(false))"#,
12002        );
12003        assert_eq!(output, vec!["1", "0"]);
12004    }
12005
12006    #[test]
12007    fn test_vm_map_len_typeof() {
12008        let output = run_output(
12009            r#"let m = map_from("a", 1)
12010print(len(m))
12011print(type_of(m))"#,
12012        );
12013        assert_eq!(output, vec!["1", "map"]);
12014    }
12015
12016    #[test]
12017    fn test_vm_json_file_roundtrip() {
12018        let output = run_output(
12019            r#"let data = map_from("name", "vm_test", "count", 99)
12020write_file("/tmp/tl_vm_json.json", json_stringify(data))
12021let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
12022print(parsed["name"])
12023print(parsed["count"])"#,
12024        );
12025        assert_eq!(output, vec!["vm_test", "99"]);
12026    }
12027
12028    // ── Phase 7: Concurrency tests ──
12029
12030    #[test]
12031    fn test_vm_spawn_await_basic() {
12032        let output = run_output(
12033            r#"fn worker() { 42 }
12034let t = spawn(worker)
12035let result = await t
12036print(result)"#,
12037        );
12038        assert_eq!(output, vec!["42"]);
12039    }
12040
12041    #[test]
12042    fn test_vm_spawn_closure_with_capture() {
12043        let output = run_output(
12044            r#"let x = 10
12045let f = () => x + 5
12046let t = spawn(f)
12047print(await t)"#,
12048        );
12049        assert_eq!(output, vec!["15"]);
12050    }
12051
12052    #[test]
12053    fn test_vm_sleep() {
12054        let output = run_output(
12055            r#"sleep(10)
12056print("done")"#,
12057        );
12058        assert_eq!(output, vec!["done"]);
12059    }
12060
12061    #[test]
12062    fn test_vm_await_non_task_passthrough() {
12063        let output = run_output(r#"print(await 42)"#);
12064        assert_eq!(output, vec!["42"]);
12065    }
12066
12067    #[test]
12068    fn test_vm_spawn_multiple_await() {
12069        let output = run_output(
12070            r#"fn w1() { 1 }
12071fn w2() { 2 }
12072fn w3() { 3 }
12073let t1 = spawn(w1)
12074let t2 = spawn(w2)
12075let t3 = spawn(w3)
12076let a = await t1
12077let b = await t2
12078let c = await t3
12079print(a + b + c)"#,
12080        );
12081        assert_eq!(output, vec!["6"]);
12082    }
12083
12084    #[test]
12085    fn test_vm_channel_basic() {
12086        let output = run_output(
12087            r#"let ch = channel()
12088send(ch, 42)
12089let val = recv(ch)
12090print(val)"#,
12091        );
12092        assert_eq!(output, vec!["42"]);
12093    }
12094
12095    #[test]
12096    fn test_vm_channel_between_tasks() {
12097        let output = run_output(
12098            r#"let ch = channel()
12099fn producer() { send(ch, 100) }
12100let t = spawn(producer)
12101let val = recv(ch)
12102await t
12103print(val)"#,
12104        );
12105        assert_eq!(output, vec!["100"]);
12106    }
12107
12108    #[test]
12109    fn test_vm_try_recv_empty() {
12110        let output = run_output(
12111            r#"let ch = channel()
12112let val = try_recv(ch)
12113print(val)"#,
12114        );
12115        assert_eq!(output, vec!["none"]);
12116    }
12117
12118    #[test]
12119    fn test_vm_channel_multiple_values() {
12120        let output = run_output(
12121            r#"let ch = channel()
12122send(ch, 1)
12123send(ch, 2)
12124send(ch, 3)
12125print(recv(ch))
12126print(recv(ch))
12127print(recv(ch))"#,
12128        );
12129        assert_eq!(output, vec!["1", "2", "3"]);
12130    }
12131
12132    #[test]
12133    fn test_vm_channel_producer_consumer() {
12134        let output = run_output(
12135            r#"let ch = channel()
12136fn producer() {
12137    send(ch, 10)
12138    send(ch, 20)
12139    send(ch, 30)
12140}
12141let t = spawn(producer)
12142let a = recv(ch)
12143let b = recv(ch)
12144let c = recv(ch)
12145await t
12146print(a + b + c)"#,
12147        );
12148        assert_eq!(output, vec!["60"]);
12149    }
12150
12151    #[test]
12152    fn test_vm_await_all() {
12153        let output = run_output(
12154            r#"fn w1() { 10 }
12155fn w2() { 20 }
12156fn w3() { 30 }
12157let t1 = spawn(w1)
12158let t2 = spawn(w2)
12159let t3 = spawn(w3)
12160let results = await_all([t1, t2, t3])
12161print(sum(results))"#,
12162        );
12163        assert_eq!(output, vec!["60"]);
12164    }
12165
12166    #[test]
12167    fn test_vm_pmap_basic() {
12168        let output = run_output(
12169            r#"let results = pmap([1, 2, 3], (x) => x * 2)
12170print(results)"#,
12171        );
12172        assert_eq!(output, vec!["[2, 4, 6]"]);
12173    }
12174
12175    #[test]
12176    fn test_vm_pmap_order_preserved() {
12177        let output = run_output(
12178            r#"let results = pmap([10, 20, 30], (x) => x + 1)
12179print(results)"#,
12180        );
12181        assert_eq!(output, vec!["[11, 21, 31]"]);
12182    }
12183
12184    #[test]
12185    fn test_vm_timeout_success() {
12186        let output = run_output(
12187            r#"fn worker() { 42 }
12188let t = spawn(worker)
12189let result = timeout(t, 5000)
12190print(result)"#,
12191        );
12192        assert_eq!(output, vec!["42"]);
12193    }
12194
12195    #[test]
12196    fn test_vm_timeout_failure() {
12197        let output = run_output(
12198            r#"fn slow() { sleep(10000) }
12199let t = spawn(slow)
12200let result = "ok"
12201try {
12202    result = timeout(t, 50)
12203} catch e {
12204    result = e
12205}
12206print(result)"#,
12207        );
12208        assert_eq!(output, vec!["Task timed out"]);
12209    }
12210
12211    #[test]
12212    fn test_vm_spawn_error_propagation() {
12213        let output = run_output(
12214            r#"fn bad() { throw "bad thing" }
12215let result = "ok"
12216try {
12217    let t = spawn(bad)
12218    result = await t
12219} catch e {
12220    result = e
12221}
12222print(result)"#,
12223        );
12224        assert_eq!(output, vec!["bad thing"]);
12225    }
12226
12227    #[test]
12228    fn test_vm_spawn_producer_consumer_pipeline() {
12229        let output = run_output(
12230            r#"let ch = channel()
12231fn producer() {
12232    let mut i = 0
12233    while i < 5 {
12234        send(ch, i * 10)
12235        i = i + 1
12236    }
12237}
12238let t = spawn(producer)
12239let mut total = 0
12240let mut count = 0
12241while count < 5 {
12242    total = total + recv(ch)
12243    count = count + 1
12244}
12245await t
12246print(total)"#,
12247        );
12248        assert_eq!(output, vec!["100"]);
12249    }
12250
12251    #[test]
12252    fn test_vm_type_of_task_channel() {
12253        let output = run_output(
12254            r#"fn worker() { 1 }
12255let t = spawn(worker)
12256let ch = channel()
12257print(type_of(t))
12258print(type_of(ch))
12259await t"#,
12260        );
12261        assert_eq!(output, vec!["task", "channel"]);
12262    }
12263
12264    // ── Phase 8: Iterators & Generators ──
12265
12266    #[test]
12267    fn test_vm_basic_generator() {
12268        let output = run_output(
12269            r#"fn gen() {
12270    yield 1
12271    yield 2
12272    yield 3
12273}
12274let g = gen()
12275print(next(g))
12276print(next(g))
12277print(next(g))
12278print(next(g))"#,
12279        );
12280        assert_eq!(output, vec!["1", "2", "3", "none"]);
12281    }
12282
12283    #[test]
12284    fn test_vm_generator_exhaustion() {
12285        let output = run_output(
12286            r#"fn gen() {
12287    yield 42
12288}
12289let g = gen()
12290print(next(g))
12291print(next(g))
12292print(next(g))"#,
12293        );
12294        assert_eq!(output, vec!["42", "none", "none"]);
12295    }
12296
12297    #[test]
12298    fn test_vm_generator_with_loop() {
12299        let output = run_output(
12300            r#"fn counter() {
12301    let mut i = 0
12302    while i < 3 {
12303        yield i
12304        i = i + 1
12305    }
12306}
12307let g = counter()
12308print(next(g))
12309print(next(g))
12310print(next(g))
12311print(next(g))"#,
12312        );
12313        assert_eq!(output, vec!["0", "1", "2", "none"]);
12314    }
12315
12316    #[test]
12317    fn test_vm_generator_with_args() {
12318        let output = run_output(
12319            r#"fn count_from(start) {
12320    let mut i = start
12321    while i < start + 3 {
12322        yield i
12323        i = i + 1
12324    }
12325}
12326let g = count_from(10)
12327print(next(g))
12328print(next(g))
12329print(next(g))
12330print(next(g))"#,
12331        );
12332        assert_eq!(output, vec!["10", "11", "12", "none"]);
12333    }
12334
12335    #[test]
12336    fn test_vm_generator_yield_none() {
12337        let output = run_output(
12338            r#"fn gen() {
12339    yield
12340    yield 5
12341}
12342let g = gen()
12343print(next(g))
12344print(next(g))
12345print(next(g))"#,
12346        );
12347        assert_eq!(output, vec!["none", "5", "none"]);
12348    }
12349
12350    #[test]
12351    fn test_vm_is_generator() {
12352        let output = run_output(
12353            r#"fn gen() { yield 1 }
12354let g = gen()
12355print(is_generator(g))
12356print(is_generator(42))
12357print(is_generator(none))"#,
12358        );
12359        assert_eq!(output, vec!["true", "false", "false"]);
12360    }
12361
12362    #[test]
12363    fn test_vm_multiple_generators() {
12364        let output = run_output(
12365            r#"fn gen() {
12366    yield 1
12367    yield 2
12368}
12369let g1 = gen()
12370let g2 = gen()
12371print(next(g1))
12372print(next(g2))
12373print(next(g1))
12374print(next(g2))"#,
12375        );
12376        assert_eq!(output, vec!["1", "1", "2", "2"]);
12377    }
12378
12379    #[test]
12380    fn test_vm_for_over_generator() {
12381        let output = run_output(
12382            r#"fn gen() {
12383    yield 10
12384    yield 20
12385    yield 30
12386}
12387for x in gen() {
12388    print(x)
12389}"#,
12390        );
12391        assert_eq!(output, vec!["10", "20", "30"]);
12392    }
12393
12394    #[test]
12395    fn test_vm_iter_builtin() {
12396        let output = run_output(
12397            r#"let g = iter([1, 2, 3])
12398print(next(g))
12399print(next(g))
12400print(next(g))
12401print(next(g))"#,
12402        );
12403        assert_eq!(output, vec!["1", "2", "3", "none"]);
12404    }
12405
12406    #[test]
12407    fn test_vm_take_builtin() {
12408        let output = run_output(
12409            r#"fn naturals() {
12410    let mut n = 0
12411    while true {
12412        yield n
12413        n = n + 1
12414    }
12415}
12416let g = take(naturals(), 5)
12417print(next(g))
12418print(next(g))
12419print(next(g))
12420print(next(g))
12421print(next(g))
12422print(next(g))"#,
12423        );
12424        assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
12425    }
12426
12427    #[test]
12428    fn test_vm_skip_builtin() {
12429        let output = run_output(
12430            r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
12431print(next(g))
12432print(next(g))
12433print(next(g))
12434print(next(g))"#,
12435        );
12436        assert_eq!(output, vec!["30", "40", "50", "none"]);
12437    }
12438
12439    #[test]
12440    fn test_vm_gen_collect() {
12441        let output = run_output(
12442            r#"fn gen() {
12443    yield 1
12444    yield 2
12445    yield 3
12446}
12447let result = gen_collect(gen())
12448print(result)"#,
12449        );
12450        assert_eq!(output, vec!["[1, 2, 3]"]);
12451    }
12452
12453    #[test]
12454    fn test_vm_gen_map() {
12455        let output = run_output(
12456            r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
12457print(gen_collect(g))"#,
12458        );
12459        assert_eq!(output, vec!["[10, 20, 30]"]);
12460    }
12461
12462    #[test]
12463    fn test_vm_gen_filter() {
12464        let output = run_output(
12465            r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
12466print(gen_collect(g))"#,
12467        );
12468        assert_eq!(output, vec!["[2, 4, 6]"]);
12469    }
12470
12471    #[test]
12472    fn test_vm_chain() {
12473        let output = run_output(
12474            r#"let g = chain(iter([1, 2]), iter([3, 4]))
12475print(gen_collect(g))"#,
12476        );
12477        assert_eq!(output, vec!["[1, 2, 3, 4]"]);
12478    }
12479
12480    #[test]
12481    fn test_vm_gen_zip() {
12482        let output = run_output(
12483            r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
12484print(gen_collect(g))"#,
12485        );
12486        assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
12487    }
12488
12489    #[test]
12490    fn test_vm_gen_enumerate() {
12491        let output = run_output(
12492            r#"let g = gen_enumerate(iter([10, 20, 30]))
12493print(gen_collect(g))"#,
12494        );
12495        assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12496    }
12497
12498    #[test]
12499    fn test_vm_combinator_chaining() {
12500        let output = run_output(
12501            r#"fn naturals() {
12502    let mut n = 0
12503    while true {
12504        yield n
12505        n = n + 1
12506    }
12507}
12508let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12509print(result)"#,
12510        );
12511        assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12512    }
12513
12514    #[test]
12515    fn test_vm_for_over_take() {
12516        let output = run_output(
12517            r#"fn naturals() {
12518    let mut n = 0
12519    while true {
12520        yield n
12521        n = n + 1
12522    }
12523}
12524for x in take(naturals(), 5) {
12525    print(x)
12526}"#,
12527        );
12528        assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12529    }
12530
12531    #[test]
12532    fn test_vm_generator_error_propagation() {
12533        let result = run(r#"fn bad_gen() {
12534    yield 1
12535    throw "oops"
12536}
12537let g = bad_gen()
12538let mut caught = ""
12539next(g)
12540try {
12541    next(g)
12542} catch e {
12543    caught = e
12544}
12545print(caught)"#);
12546        // Should succeed (error caught)
12547        assert!(result.is_ok());
12548    }
12549
12550    #[test]
12551    fn test_vm_fibonacci_generator() {
12552        let output = run_output(
12553            r#"fn fib() {
12554    let mut a = 0
12555    let mut b = 1
12556    while true {
12557        yield a
12558        let temp = a + b
12559        a = b
12560        b = temp
12561    }
12562}
12563print(gen_collect(take(fib(), 8)))"#,
12564        );
12565        assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12566    }
12567
12568    #[test]
12569    fn test_vm_generator_method_syntax() {
12570        let output = run_output(
12571            r#"fn gen() {
12572    yield 1
12573    yield 2
12574    yield 3
12575}
12576let g = gen()
12577print(type_of(g))"#,
12578        );
12579        assert_eq!(output, vec!["generator"]);
12580    }
12581
12582    // ── Phase 10: Result/Option + ? operator tests ──
12583
12584    #[test]
12585    fn test_vm_ok_err_builtins() {
12586        let output = run_output("let r = Ok(42)\nprint(r)");
12587        assert_eq!(output, vec!["Result::Ok(42)"]);
12588
12589        let output = run_output("let r = Err(\"fail\")\nprint(r)");
12590        assert_eq!(output, vec!["Result::Err(fail)"]);
12591    }
12592
12593    #[test]
12594    fn test_vm_is_ok_is_err() {
12595        let output = run_output("print(is_ok(Ok(42)))");
12596        assert_eq!(output, vec!["true"]);
12597        let output = run_output("print(is_err(Ok(42)))");
12598        assert_eq!(output, vec!["false"]);
12599        let output = run_output("print(is_ok(Err(\"fail\")))");
12600        assert_eq!(output, vec!["false"]);
12601        let output = run_output("print(is_err(Err(\"fail\")))");
12602        assert_eq!(output, vec!["true"]);
12603    }
12604
12605    #[test]
12606    fn test_vm_unwrap_ok() {
12607        let output = run_output("print(unwrap(Ok(42)))");
12608        assert_eq!(output, vec!["42"]);
12609    }
12610
12611    #[test]
12612    fn test_vm_unwrap_err_panics() {
12613        let result = run("unwrap(Err(\"fail\"))");
12614        assert!(result.is_err());
12615    }
12616
12617    #[test]
12618    fn test_vm_try_on_ok() {
12619        let output = run_output(
12620            r#"fn get_val() { Ok(42) }
12621fn process() { let v = get_val()? + 1
12622Ok(v) }
12623print(process())"#,
12624        );
12625        assert_eq!(output, vec!["Result::Ok(43)"]);
12626    }
12627
12628    #[test]
12629    fn test_vm_try_on_err_propagates() {
12630        let output = run_output(
12631            r#"fn failing() { Err("oops") }
12632fn process() { let v = failing()?
12633Ok(v) }
12634print(process())"#,
12635        );
12636        assert_eq!(output, vec!["Result::Err(oops)"]);
12637    }
12638
12639    #[test]
12640    fn test_vm_try_on_none_propagates() {
12641        let output = run_output(
12642            r#"fn get_none() { none }
12643fn process() { let v = get_none()?
1264442 }
12645print(process())"#,
12646        );
12647        assert_eq!(output, vec!["none"]);
12648    }
12649
12650    #[test]
12651    fn test_vm_try_passthrough() {
12652        // ? on a normal value should passthrough
12653        let output = run_output(
12654            r#"fn get_val() { 42 }
12655fn process() { let v = get_val()?
12656v + 1 }
12657print(process())"#,
12658        );
12659        assert_eq!(output, vec!["43"]);
12660    }
12661
12662    #[test]
12663    fn test_vm_result_match() {
12664        let output = run_output(
12665            r#"let r = Ok(42)
12666print(is_ok(r))
12667print(unwrap(r))"#,
12668        );
12669        assert_eq!(output, vec!["true", "42"]);
12670    }
12671
12672    #[test]
12673    fn test_vm_result_match_err() {
12674        let output = run_output(
12675            r#"let r = Err("fail")
12676print(is_err(r))
12677match r {
12678    Result::Err(e) => print("got error"),
12679    _ => print("no error")
12680}"#,
12681        );
12682        assert_eq!(output, vec!["true", "got error"]);
12683    }
12684
12685    // ── Set tests ──
12686
12687    #[test]
12688    fn test_vm_set_from_dedup() {
12689        let output = run_output(
12690            r#"let s = set_from([1, 2, 3, 2, 1])
12691print(len(s))
12692print(type_of(s))"#,
12693        );
12694        assert_eq!(output, vec!["3", "set"]);
12695    }
12696
12697    #[test]
12698    fn test_vm_set_add() {
12699        let output = run_output(
12700            r#"let s = set_from([1, 2])
12701let s2 = set_add(s, 3)
12702let s3 = set_add(s2, 2)
12703print(len(s2))
12704print(len(s3))"#,
12705        );
12706        assert_eq!(output, vec!["3", "3"]);
12707    }
12708
12709    #[test]
12710    fn test_vm_set_remove() {
12711        let output = run_output(
12712            r#"let s = set_from([1, 2, 3])
12713let s2 = set_remove(s, 2)
12714print(len(s2))
12715print(set_contains(s2, 2))"#,
12716        );
12717        assert_eq!(output, vec!["2", "false"]);
12718    }
12719
12720    #[test]
12721    fn test_vm_set_contains() {
12722        let output = run_output(
12723            r#"let s = set_from([1, 2, 3])
12724print(set_contains(s, 2))
12725print(set_contains(s, 5))"#,
12726        );
12727        assert_eq!(output, vec!["true", "false"]);
12728    }
12729
12730    #[test]
12731    fn test_vm_set_union() {
12732        let output = run_output(
12733            r#"let a = set_from([1, 2, 3])
12734let b = set_from([3, 4, 5])
12735let c = set_union(a, b)
12736print(len(c))"#,
12737        );
12738        assert_eq!(output, vec!["5"]);
12739    }
12740
12741    #[test]
12742    fn test_vm_set_intersection() {
12743        let output = run_output(
12744            r#"let a = set_from([1, 2, 3])
12745let b = set_from([2, 3, 4])
12746let c = set_intersection(a, b)
12747print(len(c))"#,
12748        );
12749        assert_eq!(output, vec!["2"]);
12750    }
12751
12752    #[test]
12753    fn test_vm_set_difference() {
12754        let output = run_output(
12755            r#"let a = set_from([1, 2, 3])
12756let b = set_from([2, 3, 4])
12757let c = set_difference(a, b)
12758print(len(c))"#,
12759        );
12760        assert_eq!(output, vec!["1"]);
12761    }
12762
12763    #[test]
12764    fn test_vm_set_for_loop() {
12765        let output = run_output(
12766            r#"let s = set_from([10, 20, 30])
12767let total = 0
12768for item in s {
12769    total = total + item
12770}
12771print(total)"#,
12772        );
12773        assert_eq!(output, vec!["60"]);
12774    }
12775
12776    #[test]
12777    fn test_vm_set_to_list() {
12778        let output = run_output(
12779            r#"let s = set_from([3, 1, 2])
12780let lst = s.to_list()
12781print(type_of(lst))
12782print(len(lst))"#,
12783        );
12784        assert_eq!(output, vec!["list", "3"]);
12785    }
12786
12787    #[test]
12788    fn test_vm_set_method_contains() {
12789        let output = run_output(
12790            r#"let s = set_from([1, 2, 3])
12791print(s.contains(2))
12792print(s.contains(5))"#,
12793        );
12794        assert_eq!(output, vec!["true", "false"]);
12795    }
12796
12797    #[test]
12798    fn test_vm_set_method_add_remove() {
12799        let output = run_output(
12800            r#"let s = set_from([1, 2])
12801let s2 = s.add(3)
12802print(s2.len())
12803let s3 = s2.remove(1)
12804print(s3.len())"#,
12805        );
12806        assert_eq!(output, vec!["3", "2"]);
12807    }
12808
12809    #[test]
12810    fn test_vm_set_method_union_intersection_difference() {
12811        let output = run_output(
12812            r#"let a = set_from([1, 2, 3])
12813let b = set_from([2, 3, 4])
12814print(a.union(b).len())
12815print(a.intersection(b).len())
12816print(a.difference(b).len())"#,
12817        );
12818        assert_eq!(output, vec!["4", "2", "1"]);
12819    }
12820
12821    #[test]
12822    fn test_vm_set_empty() {
12823        let output = run_output(
12824            r#"let s = set_from([])
12825print(len(s))
12826let s2 = s.add(1)
12827print(len(s2))"#,
12828        );
12829        assert_eq!(output, vec!["0", "1"]);
12830    }
12831
12832    #[test]
12833    fn test_vm_set_string_values() {
12834        let output = run_output(
12835            r#"let s = set_from(["a", "b", "a", "c"])
12836print(len(s))
12837print(s.contains("b"))"#,
12838        );
12839        assert_eq!(output, vec!["3", "true"]);
12840    }
12841
12842    // ── Phase 11: Module System VM Tests ──
12843
12844    #[test]
12845    fn test_vm_import_with_caching() {
12846        // Test that the VM has caching fields initialized
12847        let vm = Vm::new();
12848        assert!(vm.module_cache.is_empty());
12849        assert!(vm.importing_files.is_empty());
12850        assert!(vm.file_path.is_none());
12851    }
12852
12853    #[test]
12854    fn test_vm_use_single_file() {
12855        // Create a temp dir with module files
12856        let dir = tempfile::tempdir().unwrap();
12857        let lib_path = dir.path().join("math.tl");
12858        std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12859
12860        let main_path = dir.path().join("main.tl");
12861        std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12862
12863        let source = std::fs::read_to_string(&main_path).unwrap();
12864        let program = tl_parser::parse(&source).unwrap();
12865        let proto = crate::compiler::compile(&program).unwrap();
12866
12867        let mut vm = Vm::new();
12868        vm.file_path = Some(main_path.to_string_lossy().to_string());
12869        vm.execute(&proto).unwrap();
12870        assert_eq!(vm.output, vec!["3"]);
12871    }
12872
12873    #[test]
12874    fn test_vm_use_wildcard() {
12875        let dir = tempfile::tempdir().unwrap();
12876        std::fs::write(
12877            dir.path().join("helpers.tl"),
12878            "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12879        )
12880        .unwrap();
12881
12882        let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12883        let main_path = dir.path().join("main.tl");
12884        std::fs::write(&main_path, main_src).unwrap();
12885
12886        let program = tl_parser::parse(main_src).unwrap();
12887        let proto = crate::compiler::compile(&program).unwrap();
12888
12889        let mut vm = Vm::new();
12890        vm.file_path = Some(main_path.to_string_lossy().to_string());
12891        vm.execute(&proto).unwrap();
12892        assert_eq!(vm.output, vec!["hello", "bye"]);
12893    }
12894
12895    #[test]
12896    fn test_vm_use_aliased() {
12897        let dir = tempfile::tempdir().unwrap();
12898        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12899
12900        let main_src = "use mylib as m\nprint(m.compute())";
12901        let main_path = dir.path().join("main.tl");
12902        std::fs::write(&main_path, main_src).unwrap();
12903
12904        let program = tl_parser::parse(main_src).unwrap();
12905        let proto = crate::compiler::compile(&program).unwrap();
12906
12907        let mut vm = Vm::new();
12908        vm.file_path = Some(main_path.to_string_lossy().to_string());
12909        vm.execute(&proto).unwrap();
12910        assert_eq!(vm.output, vec!["42"]);
12911    }
12912
12913    #[test]
12914    fn test_vm_use_directory_module() {
12915        let dir = tempfile::tempdir().unwrap();
12916        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12917        std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12918
12919        let main_src = "use utils\nprint(helper())";
12920        let main_path = dir.path().join("main.tl");
12921        std::fs::write(&main_path, main_src).unwrap();
12922
12923        let program = tl_parser::parse(main_src).unwrap();
12924        let proto = crate::compiler::compile(&program).unwrap();
12925
12926        let mut vm = Vm::new();
12927        vm.file_path = Some(main_path.to_string_lossy().to_string());
12928        vm.execute(&proto).unwrap();
12929        assert_eq!(vm.output, vec!["99"]);
12930    }
12931
12932    #[test]
12933    fn test_vm_circular_import_detection() {
12934        let dir = tempfile::tempdir().unwrap();
12935        let a_path = dir.path().join("a.tl");
12936        let b_path = dir.path().join("b.tl");
12937        std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
12938        std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
12939
12940        let source = std::fs::read_to_string(&a_path).unwrap();
12941        let program = tl_parser::parse(&source).unwrap();
12942        let proto = crate::compiler::compile(&program).unwrap();
12943
12944        let mut vm = Vm::new();
12945        vm.file_path = Some(a_path.to_string_lossy().to_string());
12946        let result = vm.execute(&proto);
12947        assert!(result.is_err());
12948        assert!(format!("{:?}", result).contains("Circular import"));
12949    }
12950
12951    #[test]
12952    fn test_vm_module_caching() {
12953        // Import the same module twice — should use cache
12954        let dir = tempfile::tempdir().unwrap();
12955        std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
12956
12957        let main_src = "use cached\nuse cached\nprint(X)";
12958        let main_path = dir.path().join("main.tl");
12959        std::fs::write(&main_path, main_src).unwrap();
12960
12961        let program = tl_parser::parse(main_src).unwrap();
12962        let proto = crate::compiler::compile(&program).unwrap();
12963
12964        let mut vm = Vm::new();
12965        vm.file_path = Some(main_path.to_string_lossy().to_string());
12966        vm.execute(&proto).unwrap();
12967        assert_eq!(vm.output, vec!["42"]);
12968    }
12969
12970    #[test]
12971    fn test_vm_existing_import_still_works() {
12972        // Verify backward compat of classic import
12973        let dir = tempfile::tempdir().unwrap();
12974        let lib_path = dir.path().join("lib.tl");
12975        std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
12976
12977        let main_src = format!(
12978            "import \"{}\"\nprint(imported_fn())",
12979            lib_path.to_string_lossy()
12980        );
12981        let program = tl_parser::parse(&main_src).unwrap();
12982        let proto = crate::compiler::compile(&program).unwrap();
12983
12984        let mut vm = Vm::new();
12985        vm.execute(&proto).unwrap();
12986        assert_eq!(vm.output, vec!["123"]);
12987    }
12988
12989    #[test]
12990    fn test_vm_pub_fn_parsing() {
12991        // Pub fn should compile and run normally
12992        let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
12993        assert_eq!(output, vec!["3"]);
12994    }
12995
12996    #[test]
12997    fn test_vm_use_nested_path() {
12998        let dir = tempfile::tempdir().unwrap();
12999        std::fs::create_dir_all(dir.path().join("data")).unwrap();
13000        std::fs::write(
13001            dir.path().join("data/transforms.tl"),
13002            "fn clean(x) { x + 1 }",
13003        )
13004        .unwrap();
13005
13006        let main_src = "use data.transforms\nprint(clean(41))";
13007        let main_path = dir.path().join("main.tl");
13008        std::fs::write(&main_path, main_src).unwrap();
13009
13010        let program = tl_parser::parse(main_src).unwrap();
13011        let proto = crate::compiler::compile(&program).unwrap();
13012
13013        let mut vm = Vm::new();
13014        vm.file_path = Some(main_path.to_string_lossy().to_string());
13015        vm.execute(&proto).unwrap();
13016        assert_eq!(vm.output, vec!["42"]);
13017    }
13018
13019    // -- Integration tests: multi-file, backward compat, mixed --
13020
13021    #[test]
13022    fn test_integration_multi_file_use_functions() {
13023        // main.tl uses functions from lib.tl
13024        let dir = tempfile::tempdir().unwrap();
13025        std::fs::write(
13026            dir.path().join("lib.tl"),
13027            "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
13028        )
13029        .unwrap();
13030
13031        let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
13032        let main_path = dir.path().join("main.tl");
13033        std::fs::write(&main_path, main_src).unwrap();
13034
13035        let program = tl_parser::parse(main_src).unwrap();
13036        let proto = crate::compiler::compile(&program).unwrap();
13037        let mut vm = Vm::new();
13038        vm.file_path = Some(main_path.to_string_lossy().to_string());
13039        vm.execute(&proto).unwrap();
13040        assert_eq!(vm.output, vec!["Hello, World!", "42"]);
13041    }
13042
13043    #[test]
13044    fn test_integration_mixed_import_and_use() {
13045        // Combine classic import and use in same file
13046        let dir = tempfile::tempdir().unwrap();
13047        std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
13048        std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
13049
13050        let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
13051        let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
13052        let main_path = dir.path().join("main.tl");
13053        std::fs::write(&main_path, &main_src).unwrap();
13054
13055        let program = tl_parser::parse(&main_src).unwrap();
13056        let proto = crate::compiler::compile(&program).unwrap();
13057        let mut vm = Vm::new();
13058        vm.file_path = Some(main_path.to_string_lossy().to_string());
13059        vm.execute(&proto).unwrap();
13060        assert_eq!(vm.output, vec!["30"]);
13061    }
13062
13063    #[test]
13064    fn test_integration_directory_module_with_mod_tl() {
13065        // utils/mod.tl re-exports functions
13066        let dir = tempfile::tempdir().unwrap();
13067        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
13068        std::fs::write(
13069            dir.path().join("utils/mod.tl"),
13070            "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
13071        )
13072        .unwrap();
13073
13074        let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
13075        let main_path = dir.path().join("main.tl");
13076        std::fs::write(&main_path, main_src).unwrap();
13077
13078        let program = tl_parser::parse(main_src).unwrap();
13079        let proto = crate::compiler::compile(&program).unwrap();
13080        let mut vm = Vm::new();
13081        vm.file_path = Some(main_path.to_string_lossy().to_string());
13082        vm.execute(&proto).unwrap();
13083        assert_eq!(vm.output, vec!["99", "42!"]);
13084    }
13085
13086    #[test]
13087    fn test_integration_circular_dep_error() {
13088        let dir = tempfile::tempdir().unwrap();
13089        let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
13090        let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
13091        std::fs::write(
13092            dir.path().join("a.tl"),
13093            format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
13094        )
13095        .unwrap();
13096        std::fs::write(
13097            dir.path().join("b.tl"),
13098            format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
13099        )
13100        .unwrap();
13101
13102        let main_src = format!("import \"{a_abs}\"");
13103        let program = tl_parser::parse(&main_src).unwrap();
13104        let proto = crate::compiler::compile(&program).unwrap();
13105        let mut vm = Vm::new();
13106        let result = vm.execute(&proto);
13107        assert!(result.is_err());
13108        let err_msg = format!("{}", result.unwrap_err());
13109        assert!(
13110            err_msg.contains("Circular") || err_msg.contains("circular"),
13111            "Expected circular import error, got: {err_msg}"
13112        );
13113    }
13114
13115    #[test]
13116    fn test_integration_use_aliased_method_call() {
13117        // use lib as m, then m.compute()
13118        let dir = tempfile::tempdir().unwrap();
13119        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
13120
13121        let main_src = "use mylib as m\nprint(m.compute())";
13122        let main_path = dir.path().join("main.tl");
13123        std::fs::write(&main_path, main_src).unwrap();
13124
13125        let program = tl_parser::parse(main_src).unwrap();
13126        let proto = crate::compiler::compile(&program).unwrap();
13127        let mut vm = Vm::new();
13128        vm.file_path = Some(main_path.to_string_lossy().to_string());
13129        vm.execute(&proto).unwrap();
13130        assert_eq!(vm.output, vec!["42"]);
13131    }
13132
13133    #[test]
13134    fn test_integration_module_caching_shared() {
13135        // Import same module twice; second import uses cache, not re-execution
13136        let dir = tempfile::tempdir().unwrap();
13137        std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
13138
13139        let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
13140        let main_path = dir.path().join("main.tl");
13141        std::fs::write(&main_path, main_src).unwrap();
13142
13143        let program = tl_parser::parse(main_src).unwrap();
13144        let proto = crate::compiler::compile(&program).unwrap();
13145        let mut vm = Vm::new();
13146        vm.file_path = Some(main_path.to_string_lossy().to_string());
13147        vm.execute(&proto).unwrap();
13148        assert_eq!(vm.output, vec!["42", "42"]);
13149    }
13150
13151    #[test]
13152    fn test_integration_pub_keyword_in_module() {
13153        // pub fn in a module should work when imported
13154        let dir = tempfile::tempdir().unwrap();
13155        std::fs::write(
13156            dir.path().join("pubmod.tl"),
13157            "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
13158        )
13159        .unwrap();
13160
13161        let main_src = "use pubmod\nprint(public_fn())";
13162        let main_path = dir.path().join("main.tl");
13163        std::fs::write(&main_path, main_src).unwrap();
13164
13165        let program = tl_parser::parse(main_src).unwrap();
13166        let proto = crate::compiler::compile(&program).unwrap();
13167        let mut vm = Vm::new();
13168        vm.file_path = Some(main_path.to_string_lossy().to_string());
13169        vm.execute(&proto).unwrap();
13170        assert_eq!(vm.output, vec!["100"]);
13171    }
13172
13173    #[test]
13174    fn test_integration_backward_compat_import_as() {
13175        // Classic import-as syntax should still work
13176        let dir = tempfile::tempdir().unwrap();
13177        let lib_path = dir.path().join("mylib.tl");
13178        std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
13179
13180        let main_src = format!(
13181            "import \"{}\" as m\nprint(m.compute())",
13182            lib_path.to_string_lossy()
13183        );
13184        let program = tl_parser::parse(&main_src).unwrap();
13185        let proto = crate::compiler::compile(&program).unwrap();
13186        let mut vm = Vm::new();
13187        vm.execute(&proto).unwrap();
13188        assert_eq!(vm.output, vec!["77"]);
13189    }
13190
13191    // ── Phase 12: Generics & Traits (VM) ──────────────────
13192
13193    #[test]
13194    fn test_vm_generic_fn() {
13195        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
13196        assert_eq!(output, vec!["42"]);
13197    }
13198
13199    #[test]
13200    fn test_vm_generic_fn_string() {
13201        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
13202        assert_eq!(output, vec!["hello"]);
13203    }
13204
13205    #[test]
13206    fn test_vm_generic_struct() {
13207        let output = run_output(
13208            "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
13209        );
13210        assert_eq!(output, vec!["1", "hi"]);
13211    }
13212
13213    #[test]
13214    fn test_vm_trait_def_noop() {
13215        // Trait definitions should compile without error (no-op)
13216        let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
13217        assert_eq!(output, vec!["ok"]);
13218    }
13219
13220    #[test]
13221    fn test_vm_trait_impl_methods() {
13222        let output = run_output(
13223            "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())",
13224        );
13225        assert_eq!(output, vec!["point"]);
13226    }
13227
13228    #[test]
13229    fn test_vm_generic_enum() {
13230        // Generic enum declaration works — type params are erased at runtime
13231        let output = run_output(
13232            "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
13233        );
13234        assert_eq!(output, vec!["enum"]);
13235    }
13236
13237    #[test]
13238    fn test_vm_where_clause_runtime() {
13239        // Where clause is compile-time only; function still works at runtime
13240        let output =
13241            run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
13242        assert_eq!(output, vec!["10"]);
13243    }
13244
13245    #[test]
13246    fn test_vm_trait_impl_self_method() {
13247        let output = run_output(
13248            "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
13249        );
13250        assert_eq!(output, vec!["6"]);
13251    }
13252
13253    // ── Phase 12: Integration tests ──────────────────────────
13254
13255    #[test]
13256    fn test_vm_generic_fn_with_type_inference() {
13257        // Generic function called with different types
13258        let output = run_output(
13259            "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
13260        );
13261        assert_eq!(output, vec!["1", "a"]);
13262    }
13263
13264    #[test]
13265    fn test_vm_generic_struct_with_methods() {
13266        let output = run_output(
13267            "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
13268        );
13269        assert_eq!(output, vec!["42"]);
13270    }
13271
13272    #[test]
13273    fn test_vm_trait_def_impl_call() {
13274        let output = run_output(
13275            "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())",
13276        );
13277        assert_eq!(output, vec!["Alice"]);
13278    }
13279
13280    #[test]
13281    fn test_vm_multiple_generic_params() {
13282        let output = run_output(
13283            "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
13284        );
13285        assert_eq!(output, vec!["2"]);
13286    }
13287
13288    #[test]
13289    fn test_vm_backward_compat_non_generic() {
13290        // Existing non-generic code must still work unchanged
13291        let output = run_output(
13292            "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())",
13293        );
13294        assert_eq!(output, vec!["3", "7"]);
13295    }
13296
13297    // ── Phase 16: Package import resolution tests ──
13298
13299    #[test]
13300    fn test_vm_package_import_resolves() {
13301        // Create a test package on disk
13302        let tmp = tempfile::tempdir().unwrap();
13303        let pkg_dir = tmp.path().join("mylib");
13304        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13305        std::fs::write(
13306            pkg_dir.join("src/lib.tl"),
13307            "pub fn greet() { print(\"hello from pkg\") }",
13308        )
13309        .unwrap();
13310        std::fs::write(
13311            pkg_dir.join("tl.toml"),
13312            "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
13313        )
13314        .unwrap();
13315
13316        // use X imports all exports wildcard-style; call greet() directly
13317        let main_file = tmp.path().join("main.tl");
13318        std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
13319
13320        let source = std::fs::read_to_string(&main_file).unwrap();
13321        let program = tl_parser::parse(&source).unwrap();
13322        let proto = crate::compiler::compile(&program).unwrap();
13323
13324        let mut vm = Vm::new();
13325        vm.file_path = Some(main_file.to_string_lossy().to_string());
13326        vm.package_roots.insert("mylib".into(), pkg_dir);
13327        vm.execute(&proto).unwrap();
13328
13329        assert_eq!(vm.output, vec!["hello from pkg"]);
13330    }
13331
13332    #[test]
13333    fn test_vm_package_nested_import() {
13334        let tmp = tempfile::tempdir().unwrap();
13335        let pkg_dir = tmp.path().join("utils");
13336        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13337        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13338        std::fs::write(
13339            pkg_dir.join("tl.toml"),
13340            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13341        )
13342        .unwrap();
13343
13344        // use utils.math wildcard-imports math.tl contents
13345        let main_file = tmp.path().join("main.tl");
13346        std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
13347
13348        let source = std::fs::read_to_string(&main_file).unwrap();
13349        let program = tl_parser::parse(&source).unwrap();
13350        let proto = crate::compiler::compile(&program).unwrap();
13351
13352        let mut vm = Vm::new();
13353        vm.file_path = Some(main_file.to_string_lossy().to_string());
13354        vm.package_roots.insert("utils".into(), pkg_dir);
13355        vm.execute(&proto).unwrap();
13356
13357        assert_eq!(vm.output, vec!["42"]);
13358    }
13359
13360    #[test]
13361    fn test_vm_package_aliased_import() {
13362        let tmp = tempfile::tempdir().unwrap();
13363        let pkg_dir = tmp.path().join("utils");
13364        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13365        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13366        std::fs::write(
13367            pkg_dir.join("tl.toml"),
13368            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13369        )
13370        .unwrap();
13371
13372        // use X as Y creates a namespaced module object
13373        let main_file = tmp.path().join("main.tl");
13374        std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
13375
13376        let source = std::fs::read_to_string(&main_file).unwrap();
13377        let program = tl_parser::parse(&source).unwrap();
13378        let proto = crate::compiler::compile(&program).unwrap();
13379
13380        let mut vm = Vm::new();
13381        vm.file_path = Some(main_file.to_string_lossy().to_string());
13382        vm.package_roots.insert("utils".into(), pkg_dir);
13383        vm.execute(&proto).unwrap();
13384
13385        assert_eq!(vm.output, vec!["42"]);
13386    }
13387
13388    #[test]
13389    fn test_vm_package_underscore_to_hyphen() {
13390        let tmp = tempfile::tempdir().unwrap();
13391        let pkg_dir = tmp.path().join("my-pkg");
13392        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13393        std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
13394        std::fs::write(
13395            pkg_dir.join("tl.toml"),
13396            "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
13397        )
13398        .unwrap();
13399
13400        // TL identifiers use underscores, package names use hyphens
13401        let main_file = tmp.path().join("main.tl");
13402        std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
13403
13404        let source = std::fs::read_to_string(&main_file).unwrap();
13405        let program = tl_parser::parse(&source).unwrap();
13406        let proto = crate::compiler::compile(&program).unwrap();
13407
13408        let mut vm = Vm::new();
13409        vm.file_path = Some(main_file.to_string_lossy().to_string());
13410        vm.package_roots.insert("my-pkg".into(), pkg_dir);
13411        vm.execute(&proto).unwrap();
13412
13413        assert_eq!(vm.output, vec!["99"]);
13414    }
13415
13416    #[test]
13417    fn test_vm_local_module_priority_over_package() {
13418        // Local modules should take priority over packages
13419        let tmp = tempfile::tempdir().unwrap();
13420
13421        // Create a local module
13422        std::fs::write(
13423            tmp.path().join("mymod.tl"),
13424            "pub fn val() { print(\"local\") }",
13425        )
13426        .unwrap();
13427
13428        // Create a package with the same name
13429        let pkg_dir = tmp.path().join("pkg_mymod");
13430        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13431        std::fs::write(
13432            pkg_dir.join("src/lib.tl"),
13433            "pub fn val() { print(\"package\") }",
13434        )
13435        .unwrap();
13436
13437        // use mymod → wildcard imports, val() available directly
13438        let main_file = tmp.path().join("main.tl");
13439        std::fs::write(&main_file, "use mymod\nval()").unwrap();
13440
13441        let source = std::fs::read_to_string(&main_file).unwrap();
13442        let program = tl_parser::parse(&source).unwrap();
13443        let proto = crate::compiler::compile(&program).unwrap();
13444
13445        let mut vm = Vm::new();
13446        vm.file_path = Some(main_file.to_string_lossy().to_string());
13447        vm.package_roots.insert("mymod".into(), pkg_dir);
13448        vm.execute(&proto).unwrap();
13449
13450        // Local module should win
13451        assert_eq!(vm.output, vec!["local"]);
13452    }
13453
13454    #[test]
13455    fn test_vm_package_missing_error() {
13456        let tmp = tempfile::tempdir().unwrap();
13457        let main_file = tmp.path().join("main.tl");
13458        std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
13459
13460        let source = std::fs::read_to_string(&main_file).unwrap();
13461        let program = tl_parser::parse(&source).unwrap();
13462        let proto = crate::compiler::compile(&program).unwrap();
13463
13464        let mut vm = Vm::new();
13465        vm.file_path = Some(main_file.to_string_lossy().to_string());
13466        let result = vm.execute(&proto);
13467
13468        assert!(result.is_err());
13469        let err = format!("{:?}", result.unwrap_err());
13470        assert!(err.contains("Module not found"));
13471    }
13472
13473    #[test]
13474    #[cfg(feature = "native")]
13475    fn test_resolve_package_file_entry_points() {
13476        let tmp = tempfile::tempdir().unwrap();
13477
13478        // Test src/lib.tl entry point
13479        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
13480        std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
13481        let result = resolve_package_file(tmp.path(), &[]);
13482        assert!(result.is_some());
13483        assert!(result.unwrap().contains("lib.tl"));
13484
13485        // Test nested module in src/
13486        std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
13487        let result = resolve_package_file(tmp.path(), &["math"]);
13488        assert!(result.is_some());
13489        assert!(result.unwrap().contains("math.tl"));
13490    }
13491
13492    #[test]
13493    fn test_vm_package_propagates_to_sub_imports() {
13494        // Package roots should be available in sub-VM during imports
13495        let tmp = tempfile::tempdir().unwrap();
13496
13497        // Create a package
13498        let pkg_dir = tmp.path().join("helpers");
13499        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13500        std::fs::write(
13501            pkg_dir.join("src/lib.tl"),
13502            "pub fn help() { print(\"helped\") }",
13503        )
13504        .unwrap();
13505        std::fs::write(
13506            pkg_dir.join("tl.toml"),
13507            "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13508        )
13509        .unwrap();
13510
13511        // Create a local module that imports from the package (wildcard then calls directly)
13512        std::fs::write(
13513            tmp.path().join("bridge.tl"),
13514            "use helpers\npub fn run() { help() }",
13515        )
13516        .unwrap();
13517
13518        // use bridge wildcard-imports run(), then call it
13519        let main_file = tmp.path().join("main.tl");
13520        std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13521
13522        let source = std::fs::read_to_string(&main_file).unwrap();
13523        let program = tl_parser::parse(&source).unwrap();
13524        let proto = crate::compiler::compile(&program).unwrap();
13525
13526        let mut vm = Vm::new();
13527        vm.file_path = Some(main_file.to_string_lossy().to_string());
13528        vm.package_roots.insert("helpers".into(), pkg_dir);
13529        vm.execute(&proto).unwrap();
13530
13531        assert_eq!(vm.output, vec!["helped"]);
13532    }
13533
13534    // ── Phase 18: Closures & Lambdas Improvements ────────────────
13535
13536    #[test]
13537    fn test_block_body_closure_basic() {
13538        let output =
13539            run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13540        assert_eq!(output, vec!["11"]);
13541    }
13542
13543    #[test]
13544    fn test_block_body_closure_captures_upvalue() {
13545        let output = run_output(
13546            "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13547        );
13548        assert_eq!(output, vec!["15"]);
13549    }
13550
13551    #[test]
13552    fn test_block_body_closure_as_hof_arg() {
13553        let output = run_output(
13554            "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13555        );
13556        assert_eq!(output, vec!["[3, 5, 7]"]);
13557    }
13558
13559    #[test]
13560    fn test_block_body_closure_multi_stmt() {
13561        let output = run_output(
13562            "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13563        );
13564        assert_eq!(output, vec!["19"]);
13565    }
13566
13567    #[test]
13568    fn test_type_alias_noop() {
13569        // Type alias should be a no-op at runtime, code using aliased types should work
13570        let output = run_output(
13571            "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13572        );
13573        assert_eq!(output, vec!["10"]);
13574    }
13575
13576    #[test]
13577    fn test_type_alias_in_function_sig() {
13578        let output = run_output(
13579            "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13580        );
13581        assert_eq!(output, vec!["15"]);
13582    }
13583
13584    #[test]
13585    fn test_shorthand_closure() {
13586        let output = run_output("let double = x => x * 2\nprint(double(5))");
13587        assert_eq!(output, vec!["10"]);
13588    }
13589
13590    #[test]
13591    fn test_shorthand_closure_in_map() {
13592        let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13593        assert_eq!(output, vec!["[2, 4, 6]"]);
13594    }
13595
13596    #[test]
13597    fn test_iife() {
13598        let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13599        assert_eq!(output, vec!["10"]);
13600    }
13601
13602    #[test]
13603    fn test_hof_apply() {
13604        let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13605        assert_eq!(output, vec!["15"]);
13606    }
13607
13608    #[test]
13609    fn test_closure_stored_in_list() {
13610        let output = run_output(
13611            "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13612        );
13613        assert_eq!(output, vec!["6", "10"]);
13614    }
13615
13616    #[test]
13617    fn test_block_body_closure_with_return() {
13618        // Use explicit return statements since if/else is a statement, not a tail expression
13619        let output = run_output(
13620            "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13621        );
13622        assert_eq!(output, vec!["positive", "non-positive"]);
13623    }
13624
13625    #[test]
13626    fn test_shorthand_closure_in_filter() {
13627        let output = run_output(
13628            "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13629        );
13630        assert_eq!(output, vec!["[2, 4, 6]"]);
13631    }
13632
13633    #[test]
13634    fn test_block_closure_with_multiple_returns() {
13635        let output = run_output(
13636            "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13637        );
13638        assert_eq!(output, vec!["5", "3"]);
13639    }
13640
13641    #[test]
13642    fn test_type_alias_with_block_closure() {
13643        let output = run_output(
13644            "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13645        );
13646        assert_eq!(output, vec!["10"]);
13647    }
13648
13649    #[test]
13650    fn test_closure_both_backends_expr() {
13651        // Same test, just verify VM works correctly
13652        let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13653        assert_eq!(output, vec!["13"]);
13654    }
13655
13656    // Phase 20: Python FFI feature-disabled test
13657    #[test]
13658    #[cfg(not(feature = "python"))]
13659    fn test_py_feature_disabled() {
13660        let result = run("py_import(\"math\")");
13661        assert!(result.is_err());
13662        let msg = format!("{}", result.unwrap_err());
13663        assert!(msg.contains("python") && msg.contains("feature"));
13664    }
13665
13666    #[test]
13667    #[cfg(feature = "python")]
13668    fn test_vm_py_import_and_eval() {
13669        pyo3::prepare_freethreaded_python();
13670        let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13671        assert_eq!(output.len(), 1);
13672        let pi: f64 = output[0].parse().unwrap();
13673        assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13674    }
13675
13676    #[test]
13677    #[cfg(feature = "python")]
13678    fn test_vm_py_eval_arithmetic() {
13679        pyo3::prepare_freethreaded_python();
13680        let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13681        assert_eq!(output, vec!["1024"]);
13682    }
13683
13684    #[test]
13685    #[cfg(feature = "python")]
13686    fn test_vm_py_method_dispatch() {
13687        pyo3::prepare_freethreaded_python();
13688        let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13689        assert_eq!(output, vec!["5.0"]);
13690    }
13691
13692    #[test]
13693    #[cfg(feature = "python")]
13694    fn test_vm_py_list_conversion() {
13695        pyo3::prepare_freethreaded_python();
13696        let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13697        assert_eq!(output, vec!["[10, 20, 30]"]);
13698    }
13699
13700    #[test]
13701    #[cfg(feature = "python")]
13702    fn test_vm_py_none_conversion() {
13703        pyo3::prepare_freethreaded_python();
13704        let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13705        assert_eq!(output, vec!["none"]);
13706    }
13707
13708    #[test]
13709    #[cfg(feature = "python")]
13710    fn test_vm_py_error_msg_quality() {
13711        pyo3::prepare_freethreaded_python();
13712        let result = run("py_import(\"nonexistent_xyz_module\")");
13713        assert!(result.is_err());
13714        let msg = format!("{}", result.unwrap_err());
13715        assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13716    }
13717
13718    #[test]
13719    #[cfg(feature = "python")]
13720    fn test_vm_py_getattr_setattr() {
13721        pyo3::prepare_freethreaded_python();
13722        let output = run_output(
13723            "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13724        );
13725        assert_eq!(output, vec!["99"]);
13726    }
13727
13728    #[test]
13729    #[cfg(feature = "python")]
13730    fn test_vm_py_callable_round_trip() {
13731        pyo3::prepare_freethreaded_python();
13732        let output = run_output(
13733            "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13734        );
13735        assert_eq!(output, vec!["3"]);
13736    }
13737
13738    // ── Phase 21: Schema Evolution VM tests ──
13739
13740    #[test]
13741    fn test_vm_schema_register_and_get() {
13742        let source = r#"let fields = map_from("id", "int64", "name", "string")
13743schema_register("User", 1, fields)
13744let result = schema_get("User", 1)
13745print(len(result))"#;
13746        let output = run_output(source);
13747        assert_eq!(output, vec!["2"]);
13748    }
13749
13750    #[test]
13751    fn test_vm_schema_latest() {
13752        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13753schema_register("User", 2, map_from("id", "int64", "name", "string"))
13754let latest = schema_latest("User")
13755print(latest)"#;
13756        let output = run_output(source);
13757        assert_eq!(output, vec!["2"]);
13758    }
13759
13760    #[test]
13761    fn test_vm_schema_history() {
13762        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13763schema_register("User", 2, map_from("id", "int64", "name", "string"))
13764let hist = schema_history("User")
13765print(len(hist))"#;
13766        let output = run_output(source);
13767        assert_eq!(output, vec!["2"]);
13768    }
13769
13770    #[test]
13771    fn test_vm_schema_check_backward_compat() {
13772        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13773schema_register("User", 2, map_from("id", "int64", "name", "string"))
13774let issues = schema_check("User", 1, 2, "backward")
13775print(len(issues))"#;
13776        let output = run_output(source);
13777        assert_eq!(output, vec!["0"]);
13778    }
13779
13780    #[test]
13781    fn test_vm_schema_diff() {
13782        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13783schema_register("User", 2, map_from("id", "int64", "name", "string"))
13784let diffs = schema_diff("User", 1, 2)
13785print(len(diffs))"#;
13786        let output = run_output(source);
13787        assert_eq!(output, vec!["1"]);
13788    }
13789
13790    #[test]
13791    fn test_vm_schema_versions() {
13792        let source = r#"schema_register("T", 1, map_from("id", "int64"))
13793schema_register("T", 3, map_from("id", "int64"))
13794schema_register("T", 2, map_from("id", "int64"))
13795let vers = schema_versions("T")
13796print(len(vers))"#;
13797        let output = run_output(source);
13798        assert_eq!(output, vec!["3"]);
13799    }
13800
13801    #[test]
13802    fn test_vm_schema_fields() {
13803        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13804let fields = schema_fields("User", 1)
13805print(len(fields))"#;
13806        let output = run_output(source);
13807        assert_eq!(output, vec!["2"]);
13808    }
13809
13810    #[test]
13811    fn test_vm_compile_versioned_schema() {
13812        let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13813        let output = run_output(source);
13814        assert!(output[0].contains("__schema__:User:v1:"));
13815    }
13816
13817    #[test]
13818    fn test_vm_compile_migrate() {
13819        let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13820        let output = run_output(source);
13821        assert_eq!(output, vec!["ok"]);
13822    }
13823
13824    #[test]
13825    fn test_vm_schema_check_backward_compat_fails() {
13826        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13827schema_register("User", 2, map_from("id", "int64"))
13828let issues = schema_check("User", 1, 2, "backward")
13829print(len(issues))"#;
13830        let output = run_output(source);
13831        assert_eq!(output, vec!["1"]);
13832    }
13833
13834    // ── Phase 22: Decimal VM Tests ─────────────────────────────────
13835
13836    #[test]
13837    fn test_vm_decimal_literal_and_arithmetic() {
13838        let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13839        assert_eq!(output, vec!["13.0", "26.25"]);
13840    }
13841
13842    #[test]
13843    fn test_vm_decimal_div_by_zero() {
13844        let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13845        let program = tl_parser::parse(source).unwrap();
13846        let proto = crate::compile(&program).unwrap();
13847        let mut vm = Vm::new();
13848        let result = vm.execute(&proto);
13849        assert!(result.is_err());
13850    }
13851
13852    #[test]
13853    fn test_vm_decimal_comparison_ops() {
13854        let output =
13855            run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13856        assert_eq!(output, vec!["true", "false", "true"]);
13857    }
13858
13859    // ── Phase 23: Security VM Tests ────────────────────────────────
13860
13861    #[test]
13862    fn test_vm_secret_vault_crud() {
13863        let output = run_output(
13864            "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13865        );
13866        assert_eq!(output, vec!["***", "none"]);
13867    }
13868
13869    #[test]
13870    fn test_vm_mask_email_basic() {
13871        let output = run_output("print(mask_email(\"alice@domain.com\"))");
13872        assert_eq!(output, vec!["a***@domain.com"]);
13873    }
13874
13875    #[test]
13876    fn test_vm_mask_phone_basic() {
13877        let output = run_output("print(mask_phone(\"123-456-7890\"))");
13878        assert_eq!(output, vec!["***-***-7890"]);
13879    }
13880
13881    #[test]
13882    fn test_vm_mask_cc_basic() {
13883        let output = run_output("print(mask_cc(\"4111222233334444\"))");
13884        assert_eq!(output, vec!["****-****-****-4444"]);
13885    }
13886
13887    #[test]
13888    fn test_vm_hash_produces_hex() {
13889        let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13890        assert_eq!(output, vec!["64"]);
13891    }
13892
13893    #[test]
13894    fn test_vm_redact_modes() {
13895        let output =
13896            run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13897        assert_eq!(output, vec!["***", "h***o"]);
13898    }
13899
13900    #[test]
13901    fn test_vm_security_policy_sandbox() {
13902        let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13903        let program = tl_parser::parse(source).unwrap();
13904        let proto = crate::compile(&program).unwrap();
13905        let mut vm = Vm::new();
13906        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13907        vm.execute(&proto).unwrap();
13908        assert_eq!(vm.output, vec!["false", "true"]);
13909    }
13910
13911    // ── Phase 25: Async Runtime Tests (feature-gated) ──────────────
13912
13913    #[cfg(feature = "async-runtime")]
13914    #[test]
13915    fn test_vm_async_read_write_file() {
13916        let dir = tempfile::tempdir().unwrap();
13917        let path = dir.path().join("async_test.txt");
13918        let path_str = path.to_str().unwrap().replace('\\', "/");
13919        let source = format!(
13920            r#"let wt = async_write_file("{path_str}", "async hello")
13921let wr = await(wt)
13922let rt = async_read_file("{path_str}")
13923let content = await(rt)
13924print(content)"#
13925        );
13926        let output = run_output(&source);
13927        assert_eq!(output, vec!["async hello"]);
13928    }
13929
13930    #[cfg(feature = "async-runtime")]
13931    #[test]
13932    fn test_vm_async_sleep() {
13933        let source = r#"
13934let t = async_sleep(10)
13935let r = await(t)
13936print(r)
13937"#;
13938        let output = run_output(source);
13939        assert_eq!(output, vec!["none"]);
13940    }
13941
13942    #[cfg(feature = "async-runtime")]
13943    #[test]
13944    fn test_vm_select_first_wins() {
13945        // select between a fast sleep and a slow sleep — fast one wins
13946        let source = r#"
13947let fast = async_sleep(10)
13948let slow = async_sleep(5000)
13949let winner = select(fast, slow)
13950let result = await(winner)
13951print(result)
13952"#;
13953        let output = run_output(source);
13954        assert_eq!(output, vec!["none"]);
13955    }
13956
13957    #[cfg(feature = "async-runtime")]
13958    #[test]
13959    fn test_vm_race_all() {
13960        let source = r#"
13961let t1 = async_sleep(10)
13962let t2 = async_sleep(5000)
13963let winner = race_all([t1, t2])
13964let result = await(winner)
13965print(result)
13966"#;
13967        let output = run_output(source);
13968        assert_eq!(output, vec!["none"]);
13969    }
13970
13971    #[cfg(feature = "async-runtime")]
13972    #[test]
13973    fn test_vm_async_map() {
13974        let source = r#"
13975let items = [1, 2, 3]
13976let t = async_map(items, (x) => x * 10)
13977let result = await(t)
13978print(result)
13979"#;
13980        let output = run_output(source);
13981        assert_eq!(output, vec!["[10, 20, 30]"]);
13982    }
13983
13984    #[cfg(feature = "async-runtime")]
13985    #[test]
13986    fn test_vm_async_filter() {
13987        let source = r#"
13988let items = [1, 2, 3, 4, 5]
13989let t = async_filter(items, (x) => x > 3)
13990let result = await(t)
13991print(result)
13992"#;
13993        let output = run_output(source);
13994        assert_eq!(output, vec!["[4, 5]"]);
13995    }
13996
13997    #[cfg(feature = "async-runtime")]
13998    #[test]
13999    fn test_vm_async_write_file_returns_none() {
14000        let dir = tempfile::tempdir().unwrap();
14001        let path = dir.path().join("write_test.txt");
14002        let path_str = path.to_str().unwrap().replace('\\', "/");
14003        let source = format!(
14004            r#"let t = async_write_file("{path_str}", "test data")
14005let r = await(t)
14006print(r)"#
14007        );
14008        let output = run_output(&source);
14009        assert_eq!(output, vec!["none"]);
14010    }
14011
14012    #[cfg(feature = "async-runtime")]
14013    #[test]
14014    fn test_vm_async_security_policy_blocks_write() {
14015        let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
14016        let program = tl_parser::parse(source).unwrap();
14017        let proto = crate::compile(&program).unwrap();
14018        let mut vm = Vm::new();
14019        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14020        let result = vm.execute(&proto);
14021        assert!(result.is_err());
14022        let err = format!("{}", result.unwrap_err());
14023        assert!(
14024            err.contains("file_write not allowed"),
14025            "Expected security error, got: {err}"
14026        );
14027    }
14028
14029    #[cfg(feature = "async-runtime")]
14030    #[test]
14031    fn test_vm_async_security_policy_allows_read() {
14032        // Sandbox allows file_read, so async_read_file should succeed (even if file doesn't exist)
14033        let dir = tempfile::tempdir().unwrap();
14034        let path = dir.path().join("readable.txt");
14035        std::fs::write(&path, "safe content").unwrap();
14036        let path_str = path.to_str().unwrap().replace('\\', "/");
14037        let source = format!(
14038            r#"let t = async_read_file("{path_str}")
14039let r = await(t)
14040print(r)"#
14041        );
14042        let program = tl_parser::parse(&source).unwrap();
14043        let proto = crate::compile(&program).unwrap();
14044        let mut vm = Vm::new();
14045        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14046        vm.execute(&proto).unwrap();
14047        assert_eq!(vm.output, vec!["safe content"]);
14048    }
14049
14050    #[cfg(feature = "async-runtime")]
14051    #[test]
14052    fn test_vm_async_map_empty_list() {
14053        let source = r#"
14054let t = async_map([], (x) => x * 2)
14055let result = await(t)
14056print(result)
14057"#;
14058        let output = run_output(source);
14059        assert_eq!(output, vec!["[]"]);
14060    }
14061
14062    #[cfg(feature = "async-runtime")]
14063    #[test]
14064    fn test_vm_async_filter_none_match() {
14065        let source = r#"
14066let t = async_filter([1, 2, 3], (x) => x > 100)
14067let result = await(t)
14068print(result)
14069"#;
14070        let output = run_output(source);
14071        assert_eq!(output, vec!["[]"]);
14072    }
14073
14074    // --- Phase 26: Closure upvalue closing tests ---
14075
14076    #[test]
14077    fn test_vm_closure_returned_from_function() {
14078        let output = run_output(
14079            r#"
14080fn make_adder(n) {
14081    return (x) => x + n
14082}
14083let add5 = make_adder(5)
14084print(add5(3))
14085print(add5(10))
14086"#,
14087        );
14088        assert_eq!(output, vec!["8", "15"]);
14089    }
14090
14091    #[test]
14092    fn test_vm_closure_factory_multiple_calls() {
14093        let output = run_output(
14094            r#"
14095fn make_adder(n) {
14096    return (x) => x + n
14097}
14098let add2 = make_adder(2)
14099let add10 = make_adder(10)
14100print(add2(5))
14101print(add10(5))
14102print(add2(1))
14103"#,
14104        );
14105        assert_eq!(output, vec!["7", "15", "3"]);
14106    }
14107
14108    #[test]
14109    fn test_vm_closure_returned_in_list() {
14110        let output = run_output(
14111            r#"
14112fn make_ops(n) {
14113    let add = (x) => x + n
14114    let mul = (x) => x * n
14115    return [add, mul]
14116}
14117let ops = make_ops(3)
14118print(ops[0](10))
14119print(ops[1](10))
14120"#,
14121        );
14122        assert_eq!(output, vec!["13", "30"]);
14123    }
14124
14125    #[test]
14126    fn test_vm_nested_closure_return() {
14127        let output = run_output(
14128            r#"
14129fn outer(a) {
14130    fn inner(b) {
14131        return (x) => x + a + b
14132    }
14133    return inner(10)
14134}
14135let f = outer(5)
14136print(f(1))
14137"#,
14138        );
14139        assert_eq!(output, vec!["16"]);
14140    }
14141
14142    #[test]
14143    fn test_vm_multiple_closures_same_local() {
14144        let output = run_output(
14145            r#"
14146fn make_pair(n) {
14147    let inc = (x) => x + n
14148    let dec = (x) => x - n
14149    return [inc, dec]
14150}
14151let pair = make_pair(7)
14152print(pair[0](10))
14153print(pair[1](10))
14154"#,
14155        );
14156        assert_eq!(output, vec!["17", "3"]);
14157    }
14158
14159    #[test]
14160    fn test_vm_closure_captures_multiple_locals() {
14161        let output = run_output(
14162            r#"
14163fn make_greeter(greeting, name) {
14164    let sep = " "
14165    return () => greeting + sep + name
14166}
14167let hi = make_greeter("Hello", "World")
14168let bye = make_greeter("Goodbye", "Alice")
14169print(hi())
14170print(bye())
14171"#,
14172        );
14173        assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
14174    }
14175
14176    // ── Phase 27: Data Error Hierarchy tests ──
14177
14178    #[test]
14179    fn test_vm_throw_catch_preserves_enum() {
14180        let output = run_output(
14181            r#"
14182enum Color { Red, Green(x) }
14183try {
14184    throw Color::Green(42)
14185} catch e {
14186    match e {
14187        Color::Green(x) => print(x),
14188        _ => print("no match"),
14189    }
14190}
14191"#,
14192        );
14193        assert_eq!(output, vec!["42"]);
14194    }
14195
14196    #[test]
14197    fn test_vm_throw_catch_string_compat() {
14198        let output = run_output(
14199            r#"
14200try {
14201    throw "hello error"
14202} catch e {
14203    print(e)
14204}
14205"#,
14206        );
14207        assert_eq!(output, vec!["hello error"]);
14208    }
14209
14210    #[test]
14211    fn test_vm_runtime_error_still_string() {
14212        let output = run_output(
14213            r#"
14214try {
14215    let x = 1 / 0
14216} catch e {
14217    print(type_of(e))
14218}
14219"#,
14220        );
14221        assert_eq!(output, vec!["string"]);
14222    }
14223
14224    #[test]
14225    fn test_vm_data_error_construct_and_throw() {
14226        let output = run_output(
14227            r#"
14228try {
14229    throw DataError::ParseError("bad format", "file.csv")
14230} catch e {
14231    print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
14232    print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
14233}
14234"#,
14235        );
14236        assert_eq!(output, vec!["bad format", "file.csv"]);
14237    }
14238
14239    #[test]
14240    fn test_vm_network_error_construct() {
14241        let output = run_output(
14242            r#"
14243let err = NetworkError::TimeoutError("timed out")
14244match err {
14245    NetworkError::TimeoutError(msg) => print(msg),
14246    _ => print("no match"),
14247}
14248"#,
14249        );
14250        assert_eq!(output, vec!["timed out"]);
14251    }
14252
14253    #[test]
14254    fn test_vm_connector_error_construct() {
14255        let output = run_output(
14256            r#"
14257let err = ConnectorError::AuthError("invalid creds", "postgres")
14258print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
14259print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
14260"#,
14261        );
14262        assert_eq!(output, vec!["invalid creds", "postgres"]);
14263    }
14264
14265    #[test]
14266    fn test_vm_is_error_builtin() {
14267        let output = run_output(
14268            r#"
14269let e1 = DataError::NotFound("users")
14270let e2 = NetworkError::TimeoutError("slow")
14271let e3 = ConnectorError::ConfigError("bad", "redis")
14272let e4 = "not an error"
14273print(is_error(e1))
14274print(is_error(e2))
14275print(is_error(e3))
14276print(is_error(e4))
14277"#,
14278        );
14279        assert_eq!(output, vec!["true", "true", "true", "false"]);
14280    }
14281
14282    #[test]
14283    fn test_vm_error_type_builtin() {
14284        let output = run_output(
14285            r#"
14286let e1 = DataError::ParseError("bad", "x.csv")
14287let e2 = NetworkError::HttpError("fail", "url")
14288let e3 = "not an error"
14289print(error_type(e1))
14290print(error_type(e2))
14291print(error_type(e3))
14292"#,
14293        );
14294        assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
14295    }
14296
14297    #[test]
14298    fn test_vm_match_error_variants() {
14299        let output = run_output(
14300            r#"
14301fn handle(err) {
14302    print(match err {
14303        DataError::ParseError(msg, _) => "parse: " + msg,
14304        DataError::SchemaError(msg, _, _) => "schema: " + msg,
14305        DataError::ValidationError(_, field) => "validation: " + field,
14306        DataError::NotFound(name) => "not found: " + name,
14307        _ => "unknown"
14308    })
14309}
14310handle(DataError::ParseError("bad csv", "data.csv"))
14311handle(DataError::NotFound("users_table"))
14312handle(DataError::SchemaError("mismatch", "int", "string"))
14313handle(DataError::ValidationError("invalid", "email"))
14314"#,
14315        );
14316        assert_eq!(
14317            output,
14318            vec![
14319                "parse: bad csv",
14320                "not found: users_table",
14321                "schema: mismatch",
14322                "validation: email",
14323            ]
14324        );
14325    }
14326
14327    #[test]
14328    fn test_vm_rethrow_structured_error() {
14329        let output = run_output(
14330            r#"
14331try {
14332    try {
14333        throw DataError::NotFound("config")
14334    } catch e {
14335        throw e
14336    }
14337} catch outer {
14338    match outer {
14339        DataError::NotFound(name) => print("caught: " + name),
14340        _ => print("wrong type"),
14341    }
14342}
14343"#,
14344        );
14345        assert_eq!(output, vec!["caught: config"]);
14346    }
14347
14348    // ── Phase 28: Ownership & Move Semantics ──
14349
14350    #[test]
14351    fn test_vm_pipe_moves_value() {
14352        // x |> f() should consume x — accessing x after pipe gives error
14353        let result = run(r#"
14354fn identity(v) { v }
14355let x = [1, 2, 3]
14356x |> identity()
14357print(x)
14358"#);
14359        assert!(result.is_err());
14360        let err = result.unwrap_err().to_string();
14361        assert!(err.contains("moved"), "Error should mention 'moved': {err}");
14362    }
14363
14364    #[test]
14365    fn test_vm_clone_before_pipe() {
14366        // x.clone() |> f() should not consume x
14367        let output = run_output(
14368            r#"
14369fn identity(v) { v }
14370let x = [1, 2, 3]
14371x.clone() |> identity()
14372print(x)
14373"#,
14374        );
14375        assert_eq!(output, vec!["[1, 2, 3]"]);
14376    }
14377
14378    #[test]
14379    fn test_vm_clone_list_deep() {
14380        // Mutating a cloned list should not affect the original
14381        let output = run_output(
14382            r#"
14383let original = [1, 2, 3]
14384let copy = original.clone()
14385copy[0] = 99
14386print(original)
14387print(copy)
14388"#,
14389        );
14390        assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
14391    }
14392
14393    #[test]
14394    fn test_vm_clone_map() {
14395        let output = run_output(
14396            r#"
14397let m = map_from("a", 1, "b", 2)
14398let m2 = m.clone()
14399m2["a"] = 99
14400print(m)
14401print(m2)
14402"#,
14403        );
14404        assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
14405    }
14406
14407    #[test]
14408    fn test_vm_clone_struct() {
14409        let output = run_output(
14410            r#"
14411struct Point { x: int64, y: int64 }
14412let p = Point { x: 1, y: 2 }
14413let p2 = p.clone()
14414print(p)
14415print(p2)
14416"#,
14417        );
14418        assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
14419    }
14420
14421    #[test]
14422    fn test_vm_ref_read_only() {
14423        // &x should be readable but not mutable
14424        let result = run(r#"
14425let x = [1, 2, 3]
14426let r = &x
14427r[0] = 99
14428"#);
14429        assert!(result.is_err());
14430        let err = result.unwrap_err().to_string();
14431        assert!(
14432            err.contains("Cannot mutate a borrowed reference"),
14433            "Error should mention reference: {err}"
14434        );
14435    }
14436
14437    #[test]
14438    fn test_vm_ref_transparent_read() {
14439        // Reading through a ref should work transparently
14440        let output = run_output(
14441            r#"
14442let x = [1, 2, 3]
14443let r = &x
14444print(len(r))
14445"#,
14446        );
14447        assert_eq!(output, vec!["3"]);
14448    }
14449
14450    #[test]
14451    fn test_vm_parallel_for_basic() {
14452        // parallel for should iterate all elements (runs sequentially in VM)
14453        let output = run_output(
14454            r#"
14455let items = [10, 20, 30]
14456parallel for item in items {
14457    print(item)
14458}
14459"#,
14460        );
14461        assert_eq!(output, vec!["10", "20", "30"]);
14462    }
14463
14464    #[test]
14465    fn test_vm_moved_value_clear_error() {
14466        // Error message should mention .clone()
14467        let result = run(r#"
14468fn f(x) { x }
14469let data = "hello"
14470data |> f()
14471print(data)
14472"#);
14473        assert!(result.is_err());
14474        let err = result.unwrap_err().to_string();
14475        assert!(
14476            err.contains("clone()"),
14477            "Error should suggest .clone(): {err}"
14478        );
14479    }
14480
14481    #[test]
14482    fn test_vm_reassign_after_move() {
14483        // After moving, reassigning the variable should work
14484        let output = run_output(
14485            r#"
14486fn f(x) { x }
14487let x = 1
14488x |> f()
14489let x = 2
14490print(x)
14491"#,
14492        );
14493        assert_eq!(output, vec!["2"]);
14494    }
14495
14496    #[test]
14497    fn test_vm_pipe_chain_move() {
14498        // Chained pipes should work — intermediate values don't need explicit binding
14499        let output = run_output(
14500            r#"
14501fn double(x) { x * 2 }
14502fn add_one(x) { x + 1 }
14503let result = 5 |> double() |> add_one()
14504print(result)
14505"#,
14506        );
14507        assert_eq!(output, vec!["11"]);
14508    }
14509
14510    #[test]
14511    fn test_vm_string_clone() {
14512        // .clone() on string values
14513        let output = run_output(
14514            r#"
14515let s = "hello"
14516let s2 = s.clone()
14517print(s)
14518print(s2)
14519"#,
14520        );
14521        assert_eq!(output, vec!["hello", "hello"]);
14522    }
14523
14524    #[test]
14525    fn test_vm_ref_method_dispatch() {
14526        // Methods should be callable through references
14527        let output = run_output(
14528            r#"
14529let s = "hello world"
14530let r = &s
14531print(r.len())
14532"#,
14533        );
14534        assert_eq!(output, vec!["11"]);
14535    }
14536
14537    #[test]
14538    fn test_vm_ref_member_access() {
14539        // Member access through ref should work
14540        let output = run_output(
14541            r#"
14542struct Point { x: int64, y: int64 }
14543let p = Point { x: 10, y: 20 }
14544let r = &p
14545print(r.x)
14546"#,
14547        );
14548        assert_eq!(output, vec!["10"]);
14549    }
14550
14551    #[test]
14552    fn test_vm_ref_set_member_blocked() {
14553        // Setting a member through a ref should fail
14554        let result = run(r#"
14555struct Point { x: int64, y: int64 }
14556let p = Point { x: 10, y: 20 }
14557let r = &p
14558r.x = 99
14559"#);
14560        assert!(result.is_err());
14561        let err = result.unwrap_err().to_string();
14562        assert!(
14563            err.contains("Cannot mutate a borrowed reference"),
14564            "Error: {err}"
14565        );
14566    }
14567
14568    // ── Phase 29: IR Integration Tests ──
14569
14570    #[test]
14571    fn test_ir_filter_merge_chain() {
14572        // Two adjacent filters should be merged by the IR optimizer
14573        let dir = tempfile::tempdir().unwrap();
14574        let csv = dir.path().join("data.csv");
14575        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14576        let src = format!(
14577            r#"let t = read_csv("{}")
14578let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14579print(r)"#,
14580            csv.to_str().unwrap()
14581        );
14582        let output = run_output(&src);
14583        // Both Alice(30) and Charlie(35) pass both filters
14584        assert!(
14585            output[0].contains("Alice"),
14586            "Output should contain Alice: {}",
14587            output[0]
14588        );
14589        assert!(
14590            output[0].contains("Charlie"),
14591            "Output should contain Charlie: {}",
14592            output[0]
14593        );
14594        assert!(
14595            !output[0].contains("Bob"),
14596            "Output should not contain Bob: {}",
14597            output[0]
14598        );
14599    }
14600
14601    #[test]
14602    fn test_ir_predicate_pushdown_through_select() {
14603        // filter after select should be pushed before select by IR optimizer
14604        let dir = tempfile::tempdir().unwrap();
14605        let csv = dir.path().join("data.csv");
14606        std::fs::write(
14607            &csv,
14608            "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14609        )
14610        .unwrap();
14611        let src = format!(
14612            r#"let t = read_csv("{}")
14613let r = t |> select(name, age) |> filter(age > 25) |> collect()
14614print(r)"#,
14615            csv.to_str().unwrap()
14616        );
14617        let output = run_output(&src);
14618        assert!(output[0].contains("Alice"), "Output should contain Alice");
14619        assert!(
14620            output[0].contains("Charlie"),
14621            "Output should contain Charlie"
14622        );
14623        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14624    }
14625
14626    #[test]
14627    fn test_ir_sort_filter_pushdown() {
14628        // filter after sort should be pushed before sort
14629        let dir = tempfile::tempdir().unwrap();
14630        let csv = dir.path().join("data.csv");
14631        std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14632        let src = format!(
14633            r#"let t = read_csv("{}")
14634let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14635print(r)"#,
14636            csv.to_str().unwrap()
14637        );
14638        let output = run_output(&src);
14639        assert!(output[0].contains("Alice"), "Output should contain Alice");
14640        assert!(
14641            output[0].contains("Charlie"),
14642            "Output should contain Charlie"
14643        );
14644        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14645    }
14646
14647    #[test]
14648    fn test_ir_multi_operation_chain() {
14649        // Complex chain: filter + select + sort + limit
14650        let dir = tempfile::tempdir().unwrap();
14651        let csv = dir.path().join("data.csv");
14652        std::fs::write(
14653            &csv,
14654            "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14655        )
14656        .unwrap();
14657        let src = format!(
14658            r#"let t = read_csv("{}")
14659let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14660print(r)"#,
14661            csv.to_str().unwrap()
14662        );
14663        let output = run_output(&src);
14664        // Top 2 by age descending among age>22: Charlie(35), Alice(30)
14665        assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14666        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14667    }
14668
14669    #[test]
14670    fn test_ir_pipe_move_semantics_preserved() {
14671        // The source variable should be moved after pipe chain
14672        let dir = tempfile::tempdir().unwrap();
14673        let csv = dir.path().join("data.csv");
14674        std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14675        let src = format!(
14676            r#"let t = read_csv("{}")
14677let r = t |> filter(age > 0) |> collect()
14678print(t)"#,
14679            csv.to_str().unwrap()
14680        );
14681        let result = run(&src);
14682        assert!(result.is_err(), "Should error on use-after-move");
14683    }
14684
14685    #[test]
14686    fn test_ir_non_table_op_fallback() {
14687        // A pipe chain with a non-table op should fall back to legacy path
14688        let output = run_output(
14689            r#"
14690fn double(x) { x * 2 }
14691let result = 5 |> double()
14692print(result)
14693"#,
14694        );
14695        assert_eq!(output, vec!["10"]);
14696    }
14697
14698    #[test]
14699    fn test_ir_mixed_pipe_fallback() {
14700        // A pipe into a builtin (not a table op) should use legacy path
14701        let output = run_output(
14702            r#"
14703let result = [3, 1, 2] |> len()
14704print(result)
14705"#,
14706        );
14707        assert_eq!(output, vec!["3"]);
14708    }
14709
14710    #[test]
14711    fn test_ir_single_filter_roundtrip() {
14712        // Even a single filter goes through IR and round-trips correctly
14713        let dir = tempfile::tempdir().unwrap();
14714        let csv = dir.path().join("data.csv");
14715        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14716        let src = format!(
14717            r#"let t = read_csv("{}")
14718let r = t |> filter(age > 25) |> collect()
14719print(r)"#,
14720            csv.to_str().unwrap()
14721        );
14722        let output = run_output(&src);
14723        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14724        assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14725    }
14726
14727    // ── Phase 34: Agent Framework ──
14728
14729    #[test]
14730    fn test_vm_agent_definition() {
14731        let output = run_output(
14732            r#"
14733fn search(query) { "found: " + query }
14734agent bot {
14735    model: "gpt-4o",
14736    system: "You are helpful.",
14737    tools {
14738        search: {
14739            description: "Search the web",
14740            parameters: {}
14741        }
14742    },
14743    max_turns: 5
14744}
14745print(type_of(bot))
14746print(bot)
14747"#,
14748        );
14749        assert_eq!(output, vec!["agent", "<agent bot>"]);
14750    }
14751
14752    #[test]
14753    fn test_vm_agent_minimal() {
14754        let output = run_output(
14755            r#"
14756agent minimal_bot {
14757    model: "claude-sonnet-4-20250514"
14758}
14759print(type_of(minimal_bot))
14760"#,
14761        );
14762        assert_eq!(output, vec!["agent"]);
14763    }
14764
14765    #[test]
14766    fn test_vm_agent_with_base_url() {
14767        let output = run_output(
14768            r#"
14769agent local_bot {
14770    model: "llama3",
14771    base_url: "http://localhost:11434/v1",
14772    max_turns: 3
14773}
14774print(local_bot)
14775"#,
14776        );
14777        assert_eq!(output, vec!["<agent local_bot>"]);
14778    }
14779
14780    #[test]
14781    fn test_vm_agent_multiple_tools() {
14782        let output = run_output(
14783            r#"
14784fn search(query) { "result" }
14785fn weather(city) { "sunny" }
14786agent helper {
14787    model: "gpt-4o",
14788    tools {
14789        search: { description: "Search", parameters: {} },
14790        weather: { description: "Get weather", parameters: {} }
14791    }
14792}
14793print(type_of(helper))
14794"#,
14795        );
14796        assert_eq!(output, vec!["agent"]);
14797    }
14798
14799    #[test]
14800    fn test_vm_agent_lifecycle_hooks_stored() {
14801        let output = run_output(
14802            r#"
14803fn search(q) { "result" }
14804agent bot {
14805    model: "gpt-4o",
14806    tools {
14807        search: { description: "Search", parameters: {} }
14808    },
14809    on_tool_call {
14810        println("tool: " + tool_name)
14811    }
14812    on_complete {
14813        println("done")
14814    }
14815}
14816print(type_of(bot))
14817print(type_of(__agent_bot_on_tool_call__))
14818print(type_of(__agent_bot_on_complete__))
14819"#,
14820        );
14821        assert_eq!(output, vec!["agent", "function", "function"]);
14822    }
14823
14824    #[test]
14825    fn test_vm_agent_lifecycle_hook_callable() {
14826        let output = run_output(
14827            r#"
14828agent bot {
14829    model: "gpt-4o",
14830    on_tool_call {
14831        println("called: " + tool_name + " -> " + tool_result)
14832    }
14833    on_complete {
14834        println("completed")
14835    }
14836}
14837__agent_bot_on_tool_call__("search", "query", "found it")
14838__agent_bot_on_complete__("hello")
14839"#,
14840        );
14841        assert_eq!(output, vec!["called: search -> found it", "completed"]);
14842    }
14843}