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        let mut hyperparams: std::collections::HashMap<String, f64> =
7853            std::collections::HashMap::new();
7854
7855        for arg in &config_args {
7856            if let AstExpr::NamedArg { name, value } = arg {
7857                match name.as_str() {
7858                    "data" => {
7859                        data_val = Some(self.eval_ast_to_vm(value)?);
7860                    }
7861                    "target" => {
7862                        if let AstExpr::String(s) = value.as_ref() {
7863                            target_name = Some(s.clone());
7864                        }
7865                    }
7866                    "features" => {
7867                        if let AstExpr::List(items) = value.as_ref() {
7868                            for item in items {
7869                                if let AstExpr::String(s) = item {
7870                                    feature_names.push(s.clone());
7871                                }
7872                            }
7873                        }
7874                    }
7875                    // Numeric hyperparameters (n_trees, max_depth, learning_rate,
7876                    // n_estimators, alpha, k, eps, min_samples, ...). Previously
7877                    // dropped, so `train { n_trees: 80 }` silently used defaults.
7878                    other => match value.as_ref() {
7879                        AstExpr::Int(n) => {
7880                            hyperparams.insert(other.to_string(), *n as f64);
7881                        }
7882                        AstExpr::Float(f) => {
7883                            hyperparams.insert(other.to_string(), *f);
7884                        }
7885                        _ => {}
7886                    },
7887                }
7888            }
7889        }
7890
7891        // Build training config from table data
7892        let table = match data_val {
7893            Some(VmValue::Table(t)) => t,
7894            _ => return Err(runtime_err("train: data must be a table")),
7895        };
7896        let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7897
7898        // Collect table to Arrow batches
7899        let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7900        if batches.is_empty() {
7901            return Err(runtime_err("train: empty dataset"));
7902        }
7903
7904        // Schema is shared across all batches.
7905        let schema = batches[0].schema();
7906        if feature_names.is_empty() {
7907            for field in schema.fields() {
7908                if field.name() != &target {
7909                    feature_names.push(field.name().clone());
7910                }
7911            }
7912        }
7913        let n_features = feature_names.len();
7914
7915        // Resolve column indices once.
7916        let feat_idx: Vec<usize> = feature_names
7917            .iter()
7918            .map(|c| {
7919                schema
7920                    .index_of(c)
7921                    .map_err(|_| runtime_err(format!("Column not found: {c}")))
7922            })
7923            .collect::<Result<_, _>>()?;
7924        let target_idx = schema
7925            .index_of(&target)
7926            .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7927
7928        // Build the full row-major feature matrix + target across ALL batches.
7929        // Previously only batches[0] was used, so under DataFusion's parallel,
7930        // multi-partition output the model trained on a nondeterministic SUBSET of
7931        // the rows — producing flaky, sometimes-degenerate (all-one-class) models.
7932        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
7933        if total_rows == 0 {
7934            return Err(runtime_err("train: empty dataset"));
7935        }
7936        let mut row_major = Vec::with_capacity(total_rows * n_features);
7937        let mut target_data = Vec::with_capacity(total_rows);
7938        for batch in &batches {
7939            let nb = batch.num_rows();
7940            let mut cols: Vec<Vec<f64>> = Vec::with_capacity(n_features);
7941            for &ci in &feat_idx {
7942                let mut tmp = Vec::with_capacity(nb);
7943                Self::extract_f64_column(batch.column(ci), &mut tmp)?;
7944                cols.push(tmp);
7945            }
7946            for r in 0..nb {
7947                for col in &cols {
7948                    row_major.push(col[r]);
7949                }
7950            }
7951            Self::extract_f64_column(batch.column(target_idx), &mut target_data)?;
7952        }
7953
7954        // Canonicalize row order so training is reproducible regardless of the
7955        // (nondeterministic) order DataFusion returns partitions/rows in.
7956        let mut order: Vec<usize> = (0..total_rows).collect();
7957        order.sort_by(|&a, &b| {
7958            let ra = &row_major[a * n_features..(a + 1) * n_features];
7959            let rb = &row_major[b * n_features..(b + 1) * n_features];
7960            ra.iter()
7961                .zip(rb)
7962                .map(|(x, y)| x.total_cmp(y))
7963                .find(|o| o.is_ne())
7964                .unwrap_or_else(|| target_data[a].total_cmp(&target_data[b]))
7965        });
7966        let mut sorted_feats = Vec::with_capacity(total_rows * n_features);
7967        let mut sorted_target = Vec::with_capacity(total_rows);
7968        for &i in &order {
7969            sorted_feats.extend_from_slice(&row_major[i * n_features..(i + 1) * n_features]);
7970            sorted_target.push(target_data[i]);
7971        }
7972
7973        let n_rows = total_rows;
7974        let features_tensor = tl_ai::TlTensor::from_vec(sorted_feats, &[n_rows, n_features])
7975            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7976        let target_tensor = tl_ai::TlTensor::from_vec(sorted_target, &[n_rows])
7977            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7978
7979        let config = tl_ai::TrainConfig {
7980            features: features_tensor,
7981            target: target_tensor,
7982            feature_names: feature_names.clone(),
7983            target_name: target.clone(),
7984            model_name: algorithm.clone(),
7985            split_ratio: 0.8,
7986            hyperparams,
7987        };
7988
7989        let model = tl_ai::train(&algorithm, &config)
7990            .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7991
7992        Ok(VmValue::Model(Arc::new(model)))
7993    }
7994
7995    #[cfg(feature = "native")]
7996    fn extract_f64_column(
7997        col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7998        out: &mut Vec<f64>,
7999    ) -> Result<(), TlError> {
8000        use tl_data::datafusion::arrow::array::{
8001            Array, Float32Array, Float64Array, Int32Array, Int64Array,
8002        };
8003        let len = col.len();
8004        if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
8005            for i in 0..len {
8006                out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
8007            }
8008        } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
8009            for i in 0..len {
8010                out.push(if arr.is_null(i) {
8011                    0.0
8012                } else {
8013                    arr.value(i) as f64
8014                });
8015            }
8016        } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
8017            for i in 0..len {
8018                out.push(if arr.is_null(i) {
8019                    0.0
8020                } else {
8021                    arr.value(i) as f64
8022                });
8023            }
8024        } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
8025            for i in 0..len {
8026                out.push(if arr.is_null(i) {
8027                    0.0
8028                } else {
8029                    arr.value(i) as f64
8030                });
8031            }
8032        } else {
8033            return Err(runtime_err(
8034                "Column must be numeric (int32, int64, float32, float64)",
8035            ));
8036        }
8037        Ok(())
8038    }
8039
8040    #[cfg(feature = "native")]
8041    fn handle_pipeline_exec(
8042        &mut self,
8043        frame_idx: usize,
8044        name_const: u8,
8045        config_const: u8,
8046    ) -> Result<VmValue, TlError> {
8047        let frame = &self.frames[frame_idx];
8048        let name = match &frame.prototype.constants[name_const as usize] {
8049            Constant::String(s) => s.to_string(),
8050            _ => return Err(runtime_err("Expected string constant for pipeline name")),
8051        };
8052
8053        let mut schedule = None;
8054        let mut timeout_ms = None;
8055        let mut retries = 0u32;
8056
8057        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8058            for arg in args {
8059                if let AstExpr::NamedArg { name: key, value } = arg {
8060                    match key.as_str() {
8061                        "schedule" => {
8062                            if let AstExpr::String(s) = value.as_ref() {
8063                                schedule = Some(s.clone());
8064                            }
8065                        }
8066                        "timeout" => {
8067                            if let AstExpr::String(s) = value.as_ref() {
8068                                timeout_ms = tl_stream::parse_duration(s).ok();
8069                            }
8070                        }
8071                        "retries" => {
8072                            if let AstExpr::Int(n) = value.as_ref() {
8073                                retries = *n as u32;
8074                            }
8075                        }
8076                        _ => {}
8077                    }
8078                }
8079            }
8080        }
8081
8082        let def = tl_stream::PipelineDef {
8083            name,
8084            schedule,
8085            timeout_ms,
8086            retries,
8087        };
8088
8089        self.output
8090            .push(format!("Pipeline '{}': success", def.name));
8091        Ok(VmValue::PipelineDef(Arc::new(def)))
8092    }
8093
8094    #[cfg(feature = "native")]
8095    fn handle_stream_exec(
8096        &mut self,
8097        frame_idx: usize,
8098        config_const: u8,
8099    ) -> Result<VmValue, TlError> {
8100        let frame = &self.frames[frame_idx];
8101        let config_args = match &frame.prototype.constants[config_const as usize] {
8102            Constant::AstExprList(args) => args.clone(),
8103            _ => return Err(runtime_err("Expected AST expr list for stream config")),
8104        };
8105
8106        let mut name = String::new();
8107        let mut window = None;
8108        let mut watermark_ms = None;
8109
8110        for arg in &config_args {
8111            if let AstExpr::NamedArg { name: key, value } = arg {
8112                match key.as_str() {
8113                    "name" => {
8114                        if let AstExpr::String(s) = value.as_ref() {
8115                            name = s.clone();
8116                        }
8117                    }
8118                    "window" => {
8119                        if let AstExpr::String(s) = value.as_ref() {
8120                            window = Self::parse_window_type(s);
8121                        }
8122                    }
8123                    "watermark" => {
8124                        if let AstExpr::String(s) = value.as_ref() {
8125                            watermark_ms = tl_stream::parse_duration(s).ok();
8126                        }
8127                    }
8128                    _ => {}
8129                }
8130            }
8131        }
8132
8133        let def = tl_stream::StreamDef {
8134            name: name.clone(),
8135            window,
8136            watermark_ms,
8137        };
8138
8139        self.output.push(format!("Stream '{}' declared", name));
8140        Ok(VmValue::StreamDef(Arc::new(def)))
8141    }
8142
8143    #[cfg(feature = "native")]
8144    fn handle_agent_exec(
8145        &mut self,
8146        frame_idx: usize,
8147        name_const: u8,
8148        config_const: u8,
8149    ) -> Result<VmValue, TlError> {
8150        let frame = &self.frames[frame_idx];
8151        let name = match &frame.prototype.constants[name_const as usize] {
8152            Constant::String(s) => s.to_string(),
8153            _ => return Err(runtime_err("Expected string constant for agent name")),
8154        };
8155
8156        let mut model = String::new();
8157        let mut system_prompt = None;
8158        let mut max_turns = 10u32;
8159        let mut temperature = None;
8160        let mut max_tokens = None;
8161        let mut base_url = None;
8162        let mut api_key = None;
8163        let mut output_format = None;
8164        let mut tools = Vec::new();
8165        #[cfg(feature = "mcp")]
8166        let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
8167
8168        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8169            for arg in args {
8170                if let AstExpr::NamedArg { name: key, value } = arg {
8171                    if let Some(tool_name) = key.strip_prefix("tool:") {
8172                        // Tool definition — extract description and parameters from map expr
8173                        let (desc, params) = Self::extract_tool_from_ast(value);
8174                        tools.push(tl_stream::AgentTool {
8175                            name: tool_name.to_string(),
8176                            description: desc,
8177                            parameters: params,
8178                        });
8179                    } else if key.starts_with("mcp_server:") {
8180                        // MCP server reference — look up variable in globals
8181                        #[cfg(feature = "mcp")]
8182                        if let AstExpr::Ident(var_name) = value.as_ref()
8183                            && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
8184                        {
8185                            mcp_clients.push(client.clone());
8186                        }
8187                    } else {
8188                        match key.as_str() {
8189                            "model" => {
8190                                if let AstExpr::String(s) = value.as_ref() {
8191                                    model = s.clone();
8192                                }
8193                            }
8194                            "system" => {
8195                                if let AstExpr::String(s) = value.as_ref() {
8196                                    system_prompt = Some(s.clone());
8197                                }
8198                            }
8199                            "max_turns" => {
8200                                if let AstExpr::Int(n) = value.as_ref() {
8201                                    max_turns = *n as u32;
8202                                }
8203                            }
8204                            "temperature" => {
8205                                if let AstExpr::Float(f) = value.as_ref() {
8206                                    temperature = Some(*f);
8207                                }
8208                            }
8209                            "max_tokens" => {
8210                                if let AstExpr::Int(n) = value.as_ref() {
8211                                    max_tokens = Some(*n as u32);
8212                                }
8213                            }
8214                            "base_url" => {
8215                                if let AstExpr::String(s) = value.as_ref() {
8216                                    base_url = Some(s.clone());
8217                                }
8218                            }
8219                            "api_key" => {
8220                                if let AstExpr::String(s) = value.as_ref() {
8221                                    api_key = Some(s.clone());
8222                                }
8223                            }
8224                            "output_format" => {
8225                                if let AstExpr::String(s) = value.as_ref() {
8226                                    output_format = Some(s.clone());
8227                                }
8228                            }
8229                            _ => {}
8230                        }
8231                    }
8232                }
8233            }
8234        }
8235
8236        let def = tl_stream::AgentDef {
8237            name: name.clone(),
8238            model,
8239            system_prompt,
8240            tools,
8241            max_turns,
8242            temperature,
8243            max_tokens,
8244            base_url,
8245            api_key,
8246            output_format,
8247        };
8248
8249        // Store MCP clients for this agent
8250        #[cfg(feature = "mcp")]
8251        if !mcp_clients.is_empty() {
8252            self.mcp_agent_clients.insert(name.clone(), mcp_clients);
8253        }
8254
8255        Ok(VmValue::AgentDef(Arc::new(def)))
8256    }
8257
8258    #[cfg(feature = "native")]
8259    fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
8260        let mut desc = String::new();
8261        let mut params = serde_json::Value::Object(serde_json::Map::new());
8262        if let AstExpr::Map(pairs) = expr {
8263            for (key_expr, val_expr) in pairs {
8264                if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
8265                    match key.as_str() {
8266                        "description" => {
8267                            if let AstExpr::String(s) = val_expr {
8268                                desc = s.clone();
8269                            }
8270                        }
8271                        "parameters" => {
8272                            params = Self::ast_to_json(val_expr);
8273                        }
8274                        _ => {}
8275                    }
8276                }
8277            }
8278        }
8279        (desc, params)
8280    }
8281
8282    #[cfg(feature = "native")]
8283    fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
8284        match expr {
8285            AstExpr::String(s) => serde_json::Value::String(s.clone()),
8286            AstExpr::Int(n) => serde_json::json!(*n),
8287            AstExpr::Float(f) => serde_json::json!(*f),
8288            AstExpr::Bool(b) => serde_json::Value::Bool(*b),
8289            AstExpr::None => serde_json::Value::Null,
8290            AstExpr::List(items) => {
8291                serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
8292            }
8293            AstExpr::Map(pairs) => {
8294                let mut map = serde_json::Map::new();
8295                for (k, v) in pairs {
8296                    let key = match k {
8297                        AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
8298                        _ => format!("{k:?}"),
8299                    };
8300                    map.insert(key, Self::ast_to_json(v));
8301                }
8302                serde_json::Value::Object(map)
8303            }
8304            _ => serde_json::Value::Null,
8305        }
8306    }
8307
8308    #[cfg(feature = "native")]
8309    fn exec_agent_loop(
8310        &mut self,
8311        agent_def: &tl_stream::AgentDef,
8312        user_message: &str,
8313        history: Option<&[(String, String)]>,
8314    ) -> Result<VmValue, TlError> {
8315        use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
8316
8317        let model = &agent_def.model;
8318        let system = agent_def.system_prompt.as_deref();
8319        let base_url = agent_def.base_url.as_deref();
8320        let api_key = agent_def.api_key.as_deref();
8321
8322        let provider = if model.starts_with("claude") {
8323            "anthropic"
8324        } else {
8325            "openai"
8326        };
8327
8328        // Build tools JSON in OpenAI format from TL-declared tools
8329        #[allow(unused_mut)]
8330        let mut tools_json: Vec<serde_json::Value> = agent_def
8331            .tools
8332            .iter()
8333            .map(|t| {
8334                serde_json::json!({
8335                    "type": "function",
8336                    "function": {
8337                        "name": t.name,
8338                        "description": t.description,
8339                        "parameters": t.parameters
8340                    }
8341                })
8342            })
8343            .collect();
8344
8345        // Add MCP tools from connected servers
8346        #[cfg(feature = "mcp")]
8347        let mcp_clients = self
8348            .mcp_agent_clients
8349            .get(&agent_def.name)
8350            .cloned()
8351            .unwrap_or_default();
8352        #[cfg(feature = "mcp")]
8353        let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
8354            let mut dispatch = std::collections::HashMap::new();
8355            for (client_idx, client) in mcp_clients.iter().enumerate() {
8356                if let Ok(mcp_tools) = client.list_tools() {
8357                    for tool in mcp_tools {
8358                        let tool_name = tool.name.to_string();
8359                        tools_json.push(serde_json::json!({
8360                            "type": "function",
8361                            "function": {
8362                                "name": &tool_name,
8363                                "description": tool.description.as_deref().unwrap_or(""),
8364                                "parameters": serde_json::Value::Object((*tool.input_schema).clone())
8365                            }
8366                        }));
8367                        dispatch.insert(tool_name, client_idx);
8368                    }
8369                }
8370            }
8371            dispatch
8372        };
8373
8374        // Seed messages with history if provided
8375        let mut messages: Vec<serde_json::Value> = Vec::new();
8376        if let Some(hist) = history {
8377            for (role, content) in hist {
8378                messages.push(serde_json::json!({"role": role, "content": content}));
8379            }
8380        }
8381        // Add the current user message
8382        messages.push(serde_json::json!({
8383            "role": "user",
8384            "content": user_message
8385        }));
8386
8387        for turn in 0..agent_def.max_turns {
8388            let response = chat_with_tools(
8389                model,
8390                system,
8391                &messages,
8392                &tools_json,
8393                base_url,
8394                api_key,
8395                agent_def.output_format.as_deref(),
8396            )
8397            .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
8398
8399            match response {
8400                LlmResponse::Text(text) => {
8401                    // Add assistant response to history
8402                    messages.push(serde_json::json!({"role": "assistant", "content": &text}));
8403
8404                    // Build conversation history as list of [role, content] pairs
8405                    let history_list: Vec<VmValue> = messages
8406                        .iter()
8407                        .filter_map(|m| {
8408                            let role = m["role"].as_str()?;
8409                            let content = m["content"].as_str()?;
8410                            Some(VmValue::List(Box::new(vec![
8411                                VmValue::String(Arc::from(role)),
8412                                VmValue::String(Arc::from(content)),
8413                            ])))
8414                        })
8415                        .collect();
8416
8417                    // Agent completed — return result map with history
8418                    let result = VmValue::Map(Box::new(vec![
8419                        (
8420                            Arc::from("response"),
8421                            VmValue::String(Arc::from(text.as_str())),
8422                        ),
8423                        (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
8424                        (Arc::from("history"), VmValue::List(Box::new(history_list))),
8425                    ]));
8426
8427                    // Call on_complete lifecycle hook if defined
8428                    let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
8429                    if let Some(hook) = self.globals.get(&hook_name).cloned() {
8430                        let _ = self.call_value(hook, std::slice::from_ref(&result));
8431                    }
8432
8433                    return Ok(result);
8434                }
8435                LlmResponse::ToolUse(tool_calls) => {
8436                    // Add assistant message with tool calls for context
8437                    let tc_json: Vec<serde_json::Value> = tool_calls
8438                        .iter()
8439                        .map(|tc| {
8440                            serde_json::json!({
8441                                "id": tc.id,
8442                                "type": "function",
8443                                "function": {
8444                                    "name": tc.name,
8445                                    "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
8446                                }
8447                            })
8448                        })
8449                        .collect();
8450                    messages.push(serde_json::json!({
8451                        "role": "assistant",
8452                        "tool_calls": tc_json
8453                    }));
8454
8455                    // Build declared tool names (TL tools + MCP tools)
8456                    #[allow(unused_mut)]
8457                    let mut declared: Vec<String> =
8458                        agent_def.tools.iter().map(|t| t.name.clone()).collect();
8459                    #[cfg(feature = "mcp")]
8460                    {
8461                        for name in mcp_tool_dispatch.keys() {
8462                            declared.push(name.clone());
8463                        }
8464                    }
8465
8466                    // Execute each tool call
8467                    let mut results: Vec<(String, String)> = Vec::new();
8468                    for tc in &tool_calls {
8469                        if !declared.iter().any(|d| d == &tc.name) {
8470                            results.push((
8471                                tc.name.clone(),
8472                                format!("Error: '{}' not in declared tools", tc.name),
8473                            ));
8474                            continue;
8475                        }
8476
8477                        // Try MCP dispatch first, then fall back to TL function lookup
8478                        let result_str;
8479                        #[cfg(feature = "mcp")]
8480                        {
8481                            if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
8482                                let mcp_result = mcp_clients[client_idx]
8483                                    .call_tool(&tc.name, tc.input.clone())
8484                                    .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
8485                                result_str = mcp_result
8486                                    .content
8487                                    .iter()
8488                                    .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
8489                                    .collect::<Vec<_>>()
8490                                    .join("\n");
8491                            } else {
8492                                result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8493                            }
8494                        }
8495                        #[cfg(not(feature = "mcp"))]
8496                        {
8497                            result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8498                        }
8499
8500                        // Call on_tool_call lifecycle hook if defined
8501                        let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
8502                        if let Some(hook) = self.globals.get(&hook_name).cloned() {
8503                            let hook_args = vec![
8504                                VmValue::String(Arc::from(tc.name.as_str())),
8505                                self.json_value_to_vm(&tc.input),
8506                                VmValue::String(Arc::from(result_str.as_str())),
8507                            ];
8508                            let _ = self.call_value(hook, &hook_args);
8509                        }
8510
8511                        results.push((tc.name.clone(), result_str));
8512                    }
8513
8514                    // Format tool results and add to messages
8515                    let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
8516                    messages.extend(result_msgs);
8517                }
8518            }
8519        }
8520
8521        Err(runtime_err(format!(
8522            "Agent '{}' exceeded max_turns ({})",
8523            agent_def.name, agent_def.max_turns
8524        )))
8525    }
8526
8527    #[cfg(feature = "native")]
8528    fn execute_tool_call(
8529        &mut self,
8530        tool_name: &str,
8531        input: &serde_json::Value,
8532    ) -> Result<String, TlError> {
8533        // Look up the tool function in globals
8534        let func = self
8535            .globals
8536            .get(tool_name)
8537            .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8538            .clone();
8539
8540        // Convert JSON args to VmValues
8541        let args = self.json_to_vm_args(input);
8542
8543        // Call the function using call_value
8544        let result = self.call_value(func, &args)?;
8545
8546        // Convert result to string for the LLM
8547        Ok(format!("{result}"))
8548    }
8549
8550    #[cfg(feature = "native")]
8551    fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8552        match input {
8553            serde_json::Value::Object(map) => {
8554                // Pass values in order as positional args
8555                map.values().map(|v| self.json_value_to_vm(v)).collect()
8556            }
8557            serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8558            _ => vec![self.json_value_to_vm(input)],
8559        }
8560    }
8561
8562    #[cfg(feature = "native")]
8563    fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8564        match val {
8565            serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8566            serde_json::Value::Number(n) => {
8567                if let Some(i) = n.as_i64() {
8568                    VmValue::Int(i)
8569                } else if let Some(f) = n.as_f64() {
8570                    VmValue::Float(f)
8571                } else {
8572                    VmValue::None
8573                }
8574            }
8575            serde_json::Value::Bool(b) => VmValue::Bool(*b),
8576            serde_json::Value::Null => VmValue::None,
8577            serde_json::Value::Array(arr) => VmValue::List(Box::new(
8578                arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8579            )),
8580            serde_json::Value::Object(map) => {
8581                let pairs: Vec<(Arc<str>, VmValue)> = map
8582                    .iter()
8583                    .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8584                    .collect();
8585                VmValue::Map(Box::new(pairs))
8586            }
8587        }
8588    }
8589
8590    #[cfg(feature = "native")]
8591    fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8592        match &func {
8593            VmValue::Function(_) => {
8594                // Set up a synthetic call: push args to stack, do_call
8595                let save_len = self.stack.len();
8596                let func_slot = save_len;
8597                let _args_start = func_slot + 1;
8598                self.stack.push(func.clone());
8599                for arg in args {
8600                    self.stack.push(arg.clone());
8601                }
8602                self.ensure_stack(self.stack.len() + 256);
8603
8604                self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8605
8606                // Run until the function returns
8607                let entry_depth = self.frames.len() - 1;
8608                while self.frames.len() > entry_depth {
8609                    if self.run_step(entry_depth)?.is_some() {
8610                        break;
8611                    }
8612                }
8613
8614                // Result is at func_slot
8615                let result = self.stack[func_slot].clone();
8616                self.stack.truncate(save_len);
8617                Ok(result)
8618            }
8619            VmValue::Builtin(id) => {
8620                let id_u16 = *id as u16;
8621                let save_len = self.stack.len();
8622                for arg in args {
8623                    self.stack.push(arg.clone());
8624                }
8625                let result = self.call_builtin(id_u16, save_len, args.len())?;
8626                self.stack.truncate(save_len);
8627                Ok(result)
8628            }
8629            _ => Err(runtime_err(format!(
8630                "Agent tool '{}' is not callable",
8631                func.type_name()
8632            ))),
8633        }
8634    }
8635
8636    #[cfg(feature = "native")]
8637    fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8638        if let Some(dur) = s.strip_prefix("tumbling:") {
8639            let ms = tl_stream::parse_duration(dur).ok()?;
8640            Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8641        } else if let Some(rest) = s.strip_prefix("sliding:") {
8642            let parts: Vec<&str> = rest.splitn(2, ':').collect();
8643            if parts.len() == 2 {
8644                let wms = tl_stream::parse_duration(parts[0]).ok()?;
8645                let sms = tl_stream::parse_duration(parts[1]).ok()?;
8646                Some(tl_stream::window::WindowType::Sliding {
8647                    window_ms: wms,
8648                    slide_ms: sms,
8649                })
8650            } else {
8651                None
8652            }
8653        } else if let Some(dur) = s.strip_prefix("session:") {
8654            let ms = tl_stream::parse_duration(dur).ok()?;
8655            Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8656        } else {
8657            None
8658        }
8659    }
8660
8661    #[cfg(feature = "native")]
8662    fn handle_connector_decl(
8663        &mut self,
8664        frame_idx: usize,
8665        type_const: u8,
8666        config_const: u8,
8667    ) -> Result<VmValue, TlError> {
8668        let frame = &self.frames[frame_idx];
8669        let connector_type = match &frame.prototype.constants[type_const as usize] {
8670            Constant::String(s) => s.to_string(),
8671            _ => return Err(runtime_err("Expected string constant for connector type")),
8672        };
8673
8674        let config_args = match &frame.prototype.constants[config_const as usize] {
8675            Constant::AstExprList(args) => args.clone(),
8676            _ => return Err(runtime_err("Expected AST expr list for connector config")),
8677        };
8678
8679        let mut properties = std::collections::HashMap::new();
8680        for arg in &config_args {
8681            if let AstExpr::NamedArg { name: key, value } = arg {
8682                let val_str = match value.as_ref() {
8683                    AstExpr::String(s) => s.clone(),
8684                    AstExpr::Int(n) => n.to_string(),
8685                    AstExpr::Float(f) => f.to_string(),
8686                    AstExpr::Bool(b) => b.to_string(),
8687                    other => {
8688                        // Try to resolve Ident from globals
8689                        if let AstExpr::Ident(ident) = other {
8690                            if let Some(val) = self.globals.get(ident.as_str()) {
8691                                format!("{val}")
8692                            } else {
8693                                ident.clone()
8694                            }
8695                        } else {
8696                            format!("{other:?}")
8697                        }
8698                    }
8699                };
8700                properties.insert(key.clone(), val_str);
8701            }
8702        }
8703
8704        let config = tl_stream::ConnectorConfig {
8705            name: String::new(), // Will be set by SetGlobal
8706            connector_type,
8707            properties,
8708        };
8709
8710        Ok(VmValue::Connector(Arc::new(config)))
8711    }
8712
8713    /// Advance a generator by one step, returning the next value or None if done.
8714    fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8715        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8716        if gn.done {
8717            return Ok(VmValue::None);
8718        }
8719        match &mut gn.kind {
8720            GeneratorKind::UserDefined {
8721                prototype,
8722                upvalues,
8723                saved_stack,
8724                ip,
8725            } => {
8726                let proto = prototype.clone();
8727                let uvs = upvalues.clone();
8728                let stack_snapshot = saved_stack.clone();
8729                let saved_ip = *ip;
8730                drop(gn); // release lock before running VM
8731
8732                // Set up a frame to resume the generator
8733                let new_base = self.stack.len();
8734                let num_regs = proto.num_registers as usize;
8735                self.ensure_stack(new_base + num_regs + 1);
8736                // Restore saved registers
8737                for (i, val) in stack_snapshot.iter().enumerate() {
8738                    self.stack[new_base + i] = val.clone();
8739                }
8740
8741                self.frames.push(CallFrame {
8742                    prototype: proto,
8743                    ip: saved_ip,
8744                    base: new_base,
8745                    upvalues: uvs,
8746                });
8747
8748                self.yielded_value = None;
8749                let _result = self.run()?;
8750
8751                if let Some(yielded) = self.yielded_value.take() {
8752                    // Generator yielded — save state back
8753                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8754                    if let GeneratorKind::UserDefined {
8755                        saved_stack, ip, ..
8756                    } = &mut gn.kind
8757                    {
8758                        // Save the current register state
8759                        let num_regs_save = saved_stack.len();
8760                        for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8761                            if new_base + i < self.stack.len() {
8762                                *slot = self.stack[new_base + i].clone();
8763                            }
8764                        }
8765                        // Save the ip (instruction after yield)
8766                        *ip = self.yielded_ip;
8767                    }
8768                    self.stack.truncate(new_base);
8769                    Ok(yielded)
8770                } else {
8771                    // Generator returned (no yield) — mark done
8772                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8773                    gn.done = true;
8774                    self.stack.truncate(new_base);
8775                    Ok(VmValue::None)
8776                }
8777            }
8778            GeneratorKind::ListIter { items, index } => {
8779                if *index < items.len() {
8780                    let val = items[*index].clone();
8781                    *index += 1;
8782                    Ok(val)
8783                } else {
8784                    gn.done = true;
8785                    Ok(VmValue::None)
8786                }
8787            }
8788            GeneratorKind::Take { source, remaining } => {
8789                if *remaining == 0 {
8790                    gn.done = true;
8791                    return Ok(VmValue::None);
8792                }
8793                *remaining -= 1;
8794                let src = source.clone();
8795                drop(gn);
8796                let val = self.generator_next(&src)?;
8797                if matches!(val, VmValue::None) {
8798                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8799                    gn.done = true;
8800                }
8801                Ok(val)
8802            }
8803            GeneratorKind::Skip { source, remaining } => {
8804                let src = source.clone();
8805                let skip_n = *remaining;
8806                *remaining = 0;
8807                drop(gn);
8808                // Skip initial values
8809                for _ in 0..skip_n {
8810                    let val = self.generator_next(&src)?;
8811                    if matches!(val, VmValue::None) {
8812                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8813                        gn.done = true;
8814                        return Ok(VmValue::None);
8815                    }
8816                }
8817                let val = self.generator_next(&src)?;
8818                if matches!(val, VmValue::None) {
8819                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8820                    gn.done = true;
8821                }
8822                Ok(val)
8823            }
8824            GeneratorKind::Map { source, func } => {
8825                let src = source.clone();
8826                let f = func.clone();
8827                drop(gn);
8828                let val = self.generator_next(&src)?;
8829                if matches!(val, VmValue::None) {
8830                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8831                    gn.done = true;
8832                    return Ok(VmValue::None);
8833                }
8834                self.call_vm_function(&f, &[val])
8835            }
8836            GeneratorKind::Filter { source, func } => {
8837                let src = source.clone();
8838                let f = func.clone();
8839                drop(gn);
8840                loop {
8841                    let val = self.generator_next(&src)?;
8842                    if matches!(val, VmValue::None) {
8843                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8844                        gn.done = true;
8845                        return Ok(VmValue::None);
8846                    }
8847                    let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8848                    if test.is_truthy() {
8849                        return Ok(val);
8850                    }
8851                }
8852            }
8853            GeneratorKind::Chain {
8854                first,
8855                second,
8856                on_second,
8857            } => {
8858                if !*on_second {
8859                    let src = first.clone();
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                        if let GeneratorKind::Chain {
8865                            on_second, second, ..
8866                        } = &mut gn.kind
8867                        {
8868                            *on_second = true;
8869                            let src2 = second.clone();
8870                            drop(gn);
8871                            return self.generator_next(&src2);
8872                        }
8873                    }
8874                    Ok(val)
8875                } else {
8876                    let src = second.clone();
8877                    drop(gn);
8878                    let val = self.generator_next(&src)?;
8879                    if matches!(val, VmValue::None) {
8880                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8881                        gn.done = true;
8882                    }
8883                    Ok(val)
8884                }
8885            }
8886            GeneratorKind::Zip { first, second } => {
8887                let src1 = first.clone();
8888                let src2 = second.clone();
8889                drop(gn);
8890                let val1 = self.generator_next(&src1)?;
8891                let val2 = self.generator_next(&src2)?;
8892                if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8893                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8894                    gn.done = true;
8895                    return Ok(VmValue::None);
8896                }
8897                Ok(VmValue::List(Box::new(vec![val1, val2])))
8898            }
8899            GeneratorKind::Enumerate { source, index } => {
8900                let src = source.clone();
8901                let idx = *index;
8902                *index += 1;
8903                drop(gn);
8904                let val = self.generator_next(&src)?;
8905                if matches!(val, VmValue::None) {
8906                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8907                    gn.done = true;
8908                    return Ok(VmValue::None);
8909                }
8910                Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8911            }
8912        }
8913    }
8914
8915    /// Process a __schema__:Name:vN:fields... global to register in schema_registry.
8916    #[cfg(feature = "native")]
8917    fn process_schema_global(&mut self, s: &str) {
8918        // Format: __schema__:Name:vN:field1:Type,field2:Type,...
8919        let rest = &s["__schema__:".len()..];
8920        let parts: Vec<&str> = rest.splitn(3, ':').collect();
8921        if parts.len() < 2 {
8922            return;
8923        }
8924
8925        let schema_name = parts[0];
8926        let mut version: i64 = 0;
8927        let fields_str;
8928
8929        if parts.len() == 3 && parts[1].starts_with('v') {
8930            // Versioned: Name:vN:fields
8931            version = parts[1][1..].parse().unwrap_or(0);
8932            fields_str = parts[2];
8933        } else if parts.len() == 3 {
8934            // No version prefix, treat as v0: Name:field1:...
8935            fields_str = &rest[schema_name.len() + 1..];
8936        } else {
8937            fields_str = parts[1];
8938        }
8939
8940        if version == 0 {
8941            return;
8942        } // Only register versioned schemas
8943
8944        let mut arrow_fields = Vec::new();
8945        for field_pair in fields_str.split(',') {
8946            let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8947            if kv.len() == 2 {
8948                let fname = kv[0].trim();
8949                let ftype = kv[1].trim();
8950                // Parse type expr debug format: Simple("typename")
8951                let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8952                    &ftype[8..ftype.len() - 2]
8953                } else {
8954                    ftype
8955                };
8956                let dt = crate::schema::type_name_to_arrow_pub(type_name);
8957                arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8958            }
8959        }
8960
8961        if !arrow_fields.is_empty() {
8962            let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8963            let _ = self.schema_registry.register(
8964                schema_name,
8965                version,
8966                schema,
8967                crate::schema::SchemaMetadata::default(),
8968            );
8969        }
8970    }
8971
8972    /// Process a __migrate__:Name:fromVer:toVer:ops global to apply migration.
8973    #[cfg(feature = "native")]
8974    fn process_migrate_global(&mut self, s: &str) {
8975        // Format: __migrate__:Name:from:to:op1;op2;...
8976        let rest = &s["__migrate__:".len()..];
8977        let parts: Vec<&str> = rest.splitn(4, ':').collect();
8978        if parts.len() < 4 {
8979            return;
8980        }
8981
8982        let schema_name = parts[0];
8983        let from_ver: i64 = parts[1].parse().unwrap_or(0);
8984        let to_ver: i64 = parts[2].parse().unwrap_or(0);
8985        let ops_str = parts[3];
8986
8987        let mut ops = Vec::new();
8988        for op_str in ops_str.split(';') {
8989            let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8990            if op_parts.is_empty() {
8991                continue;
8992            }
8993            match op_parts[0] {
8994                "add" if op_parts.len() >= 3 => {
8995                    let name = op_parts[1].to_string();
8996                    // Parse type from debug format: Simple("typename")
8997                    let type_raw = op_parts[2];
8998                    let type_name =
8999                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
9000                            type_raw[8..type_raw.len() - 2].to_string()
9001                        } else {
9002                            type_raw.to_string()
9003                        };
9004                    let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
9005                        Some(
9006                            op_parts[3]["default:".len()..]
9007                                .trim_matches('"')
9008                                .to_string(),
9009                        )
9010                    } else {
9011                        None
9012                    };
9013                    ops.push(crate::schema::MigrationOp::AddColumn {
9014                        name,
9015                        type_name,
9016                        default,
9017                    });
9018                }
9019                "drop" if op_parts.len() >= 2 => {
9020                    ops.push(crate::schema::MigrationOp::DropColumn {
9021                        name: op_parts[1].to_string(),
9022                    });
9023                }
9024                "rename" if op_parts.len() >= 3 => {
9025                    ops.push(crate::schema::MigrationOp::RenameColumn {
9026                        from: op_parts[1].to_string(),
9027                        to: op_parts[2].to_string(),
9028                    });
9029                }
9030                "alter" if op_parts.len() >= 3 => {
9031                    let type_raw = op_parts[2];
9032                    let type_name =
9033                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
9034                            type_raw[8..type_raw.len() - 2].to_string()
9035                        } else {
9036                            type_raw.to_string()
9037                        };
9038                    ops.push(crate::schema::MigrationOp::AlterType {
9039                        column: op_parts[1].to_string(),
9040                        new_type: type_name,
9041                    });
9042                }
9043                _ => {}
9044            }
9045        }
9046
9047        let _ = self
9048            .schema_registry
9049            .apply_migration(schema_name, from_ver, to_ver, &ops);
9050    }
9051
9052    /// Dispatch a method call on an object.
9053    /// Deep-clone a VmValue, recursively copying containers.
9054    fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
9055        match val {
9056            VmValue::List(items) => {
9057                let cloned: Result<Vec<_>, _> =
9058                    items.iter().map(|v| self.deep_clone_value(v)).collect();
9059                Ok(VmValue::List(Box::new(cloned?)))
9060            }
9061            VmValue::Map(pairs) => {
9062                let cloned: Result<Vec<_>, _> = pairs
9063                    .iter()
9064                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9065                    .collect();
9066                Ok(VmValue::Map(Box::new(cloned?)))
9067            }
9068            VmValue::Set(items) => {
9069                let cloned: Result<Vec<_>, _> =
9070                    items.iter().map(|v| self.deep_clone_value(v)).collect();
9071                Ok(VmValue::Set(Box::new(cloned?)))
9072            }
9073            VmValue::StructInstance(inst) => {
9074                let cloned_fields: Result<Vec<_>, _> = inst
9075                    .fields
9076                    .iter()
9077                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9078                    .collect();
9079                Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
9080                    type_name: inst.type_name.clone(),
9081                    fields: cloned_fields?,
9082                })))
9083            }
9084            VmValue::EnumInstance(e) => {
9085                let cloned_fields: Result<Vec<_>, _> =
9086                    e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
9087                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
9088                    type_name: e.type_name.clone(),
9089                    variant: e.variant.clone(),
9090                    fields: cloned_fields?,
9091                })))
9092            }
9093            #[cfg(feature = "gpu")]
9094            VmValue::GpuTensor(gt) => {
9095                let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
9096                Ok(VmValue::GpuTensor(Arc::new(cloned)))
9097            }
9098            VmValue::Ref(inner) => self.deep_clone_value(inner),
9099            VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
9100            VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
9101            VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
9102            VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
9103            other => Ok(other.clone()),
9104        }
9105    }
9106
9107    pub fn dispatch_method(
9108        &mut self,
9109        obj: VmValue,
9110        method: &str,
9111        args: &[VmValue],
9112    ) -> Result<VmValue, TlError> {
9113        // Universal .clone() method — deep copy any value
9114        if method == "clone" {
9115            return self.deep_clone_value(&obj);
9116        }
9117        // Unwrap Ref for method dispatch — methods can be called through references
9118        let obj = match obj {
9119            VmValue::Ref(inner) => inner.as_ref().clone(),
9120            other => other,
9121        };
9122        match &obj {
9123            VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
9124            VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
9125            VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
9126            VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
9127            VmValue::Module(m) => {
9128                if let Some(func) = m.exports.get(method).cloned() {
9129                    self.call_vm_function(&func, args)
9130                } else {
9131                    Err(runtime_err(format!(
9132                        "Module '{}' has no export '{}'",
9133                        m.name, method
9134                    )))
9135                }
9136            }
9137            VmValue::StructInstance(inst) => {
9138                // Look up impl method: Type::method in globals
9139                let mangled = format!("{}::{}", inst.type_name, method);
9140                if let Some(func) = self.globals.get(&mangled).cloned() {
9141                    let mut all_args = vec![obj.clone()];
9142                    all_args.extend_from_slice(args);
9143                    self.call_vm_function(&func, &all_args)
9144                } else {
9145                    Err(runtime_err(format!(
9146                        "No method '{}' on struct '{}'",
9147                        method, inst.type_name
9148                    )))
9149                }
9150            }
9151            #[cfg(feature = "python")]
9152            VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
9153            #[cfg(feature = "gpu")]
9154            VmValue::GpuTensor(gt) => match method {
9155                "to_cpu" => {
9156                    let cpu = gt.to_cpu().map_err(runtime_err)?;
9157                    Ok(VmValue::Tensor(Arc::new(cpu)))
9158                }
9159                "shape" => {
9160                    let shape_list = Box::new(
9161                        gt.shape
9162                            .iter()
9163                            .map(|&d| VmValue::Int(d as i64))
9164                            .collect::<Vec<_>>(),
9165                    );
9166                    Ok(VmValue::List(shape_list))
9167                }
9168                "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
9169                _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
9170            },
9171            _ => {
9172                // Try looking up Type::method from type_name
9173                let type_name = obj.type_name();
9174                let mangled = format!("{}::{}", type_name, method);
9175                if let Some(func) = self.globals.get(&mangled).cloned() {
9176                    let mut all_args = vec![obj];
9177                    all_args.extend_from_slice(args);
9178                    self.call_vm_function(&func, &all_args)
9179                } else {
9180                    Err(runtime_err(format!(
9181                        "No method '{}' on type '{}'",
9182                        method, type_name
9183                    )))
9184                }
9185            }
9186        }
9187    }
9188
9189    /// Dispatch string methods.
9190    fn dispatch_string_method(
9191        &self,
9192        s: Arc<str>,
9193        method: &str,
9194        args: &[VmValue],
9195    ) -> Result<VmValue, TlError> {
9196        match method {
9197            "len" => Ok(VmValue::Int(s.len() as i64)),
9198            "split" => {
9199                let sep = match args.first() {
9200                    Some(VmValue::String(sep)) => sep.to_string(),
9201                    _ => return Err(runtime_err("split() expects a string separator")),
9202                };
9203                let parts: Vec<VmValue> = s
9204                    .split(&sep)
9205                    .map(|p| VmValue::String(Arc::from(p)))
9206                    .collect();
9207                Ok(VmValue::List(Box::new(parts)))
9208            }
9209            "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
9210            "contains" => {
9211                let needle = match args.first() {
9212                    Some(VmValue::String(n)) => n.to_string(),
9213                    _ => return Err(runtime_err("contains() expects a string")),
9214                };
9215                Ok(VmValue::Bool(s.contains(&needle)))
9216            }
9217            "replace" => {
9218                if args.len() < 2 {
9219                    return Err(runtime_err("replace() expects 2 arguments (old, new)"));
9220                }
9221                let old = match &args[0] {
9222                    VmValue::String(s) => s.to_string(),
9223                    _ => return Err(runtime_err("replace() arg must be string")),
9224                };
9225                let new = match &args[1] {
9226                    VmValue::String(s) => s.to_string(),
9227                    _ => return Err(runtime_err("replace() arg must be string")),
9228                };
9229                Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
9230            }
9231            "starts_with" => {
9232                let prefix = match args.first() {
9233                    Some(VmValue::String(p)) => p.to_string(),
9234                    _ => return Err(runtime_err("starts_with() expects a string")),
9235                };
9236                Ok(VmValue::Bool(s.starts_with(&prefix)))
9237            }
9238            "ends_with" => {
9239                let suffix = match args.first() {
9240                    Some(VmValue::String(p)) => p.to_string(),
9241                    _ => return Err(runtime_err("ends_with() expects a string")),
9242                };
9243                Ok(VmValue::Bool(s.ends_with(&suffix)))
9244            }
9245            "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
9246            "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
9247            "chars" => {
9248                let chars: Vec<VmValue> = s
9249                    .chars()
9250                    .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
9251                    .collect();
9252                Ok(VmValue::List(Box::new(chars)))
9253            }
9254            "repeat" => {
9255                let n = match args.first() {
9256                    Some(VmValue::Int(n)) => *n as usize,
9257                    _ => return Err(runtime_err("repeat() expects an integer")),
9258                };
9259                Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
9260            }
9261            "index_of" => {
9262                let needle = match args.first() {
9263                    Some(VmValue::String(n)) => n.to_string(),
9264                    _ => return Err(runtime_err("index_of() expects a string")),
9265                };
9266                Ok(VmValue::Int(
9267                    s.find(&needle).map(|i| i as i64).unwrap_or(-1),
9268                ))
9269            }
9270            "substring" => {
9271                if args.len() < 2 {
9272                    return Err(runtime_err("substring() expects start and end"));
9273                }
9274                let start = match &args[0] {
9275                    VmValue::Int(n) => *n as usize,
9276                    _ => return Err(runtime_err("substring() expects integers")),
9277                };
9278                let end = match &args[1] {
9279                    VmValue::Int(n) => *n as usize,
9280                    _ => return Err(runtime_err("substring() expects integers")),
9281                };
9282                let end = end.min(s.len());
9283                let start = start.min(end);
9284                Ok(VmValue::String(Arc::from(&s[start..end])))
9285            }
9286            "pad_left" => {
9287                if args.is_empty() {
9288                    return Err(runtime_err("pad_left() expects width"));
9289                }
9290                let width = match &args[0] {
9291                    VmValue::Int(n) => *n as usize,
9292                    _ => return Err(runtime_err("pad_left() expects integer width")),
9293                };
9294                let ch = match args.get(1) {
9295                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9296                    _ => ' ',
9297                };
9298                if s.len() >= width {
9299                    Ok(VmValue::String(s))
9300                } else {
9301                    Ok(VmValue::String(Arc::from(
9302                        format!(
9303                            "{}{}",
9304                            std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
9305                            s
9306                        )
9307                        .as_str(),
9308                    )))
9309                }
9310            }
9311            "pad_right" => {
9312                if args.is_empty() {
9313                    return Err(runtime_err("pad_right() expects width"));
9314                }
9315                let width = match &args[0] {
9316                    VmValue::Int(n) => *n as usize,
9317                    _ => return Err(runtime_err("pad_right() expects integer width")),
9318                };
9319                let ch = match args.get(1) {
9320                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9321                    _ => ' ',
9322                };
9323                if s.len() >= width {
9324                    Ok(VmValue::String(s))
9325                } else {
9326                    Ok(VmValue::String(Arc::from(
9327                        format!(
9328                            "{}{}",
9329                            s,
9330                            std::iter::repeat_n(ch, width - s.len()).collect::<String>()
9331                        )
9332                        .as_str(),
9333                    )))
9334                }
9335            }
9336            "join" => {
9337                // "sep".join(list) -> string
9338                let items = match args.first() {
9339                    Some(VmValue::List(items)) => items,
9340                    _ => return Err(runtime_err("join() expects a list")),
9341                };
9342                let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
9343                Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
9344            }
9345            "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
9346            "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
9347            "count" => {
9348                if args.is_empty() {
9349                    return Err(runtime_err("count() expects a substring"));
9350                }
9351                if let VmValue::String(sub) = &args[0] {
9352                    Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
9353                } else {
9354                    Err(runtime_err("count() expects a string"))
9355                }
9356            }
9357            "is_empty" => Ok(VmValue::Bool(s.is_empty())),
9358            "is_numeric" => Ok(VmValue::Bool(
9359                s.chars()
9360                    .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
9361            )),
9362            "is_alpha" => Ok(VmValue::Bool(
9363                !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
9364            )),
9365            "strip_prefix" => {
9366                if args.is_empty() {
9367                    return Err(runtime_err("strip_prefix() expects a string"));
9368                }
9369                if let VmValue::String(prefix) = &args[0] {
9370                    match s.strip_prefix(prefix.as_ref()) {
9371                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9372                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9373                    }
9374                } else {
9375                    Err(runtime_err("strip_prefix() expects a string"))
9376                }
9377            }
9378            "strip_suffix" => {
9379                if args.is_empty() {
9380                    return Err(runtime_err("strip_suffix() expects a string"));
9381                }
9382                if let VmValue::String(suffix) = &args[0] {
9383                    match s.strip_suffix(suffix.as_ref()) {
9384                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9385                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9386                    }
9387                } else {
9388                    Err(runtime_err("strip_suffix() expects a string"))
9389                }
9390            }
9391            _ => Err(runtime_err(format!("No method '{}' on string", method))),
9392        }
9393    }
9394
9395    /// Dispatch list methods.
9396    fn dispatch_list_method(
9397        &mut self,
9398        items: Vec<VmValue>,
9399        method: &str,
9400        args: &[VmValue],
9401    ) -> Result<VmValue, TlError> {
9402        match method {
9403            "len" => Ok(VmValue::Int(items.len() as i64)),
9404            "push" => {
9405                if args.is_empty() {
9406                    return Err(runtime_err("push() expects 1 argument"));
9407                }
9408                let mut new_items = items;
9409                new_items.push(args[0].clone());
9410                Ok(VmValue::List(Box::new(new_items)))
9411            }
9412            "map" => {
9413                if args.is_empty() {
9414                    return Err(runtime_err("map() expects a function"));
9415                }
9416                let func = &args[0];
9417                let mut result = Vec::new();
9418                for item in items {
9419                    let val = self.call_vm_function(func, &[item])?;
9420                    result.push(val);
9421                }
9422                Ok(VmValue::List(Box::new(result)))
9423            }
9424            "filter" => {
9425                if args.is_empty() {
9426                    return Err(runtime_err("filter() expects a function"));
9427                }
9428                let func = &args[0];
9429                let mut result = Vec::new();
9430                for item in items {
9431                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9432                    if val.is_truthy() {
9433                        result.push(item);
9434                    }
9435                }
9436                Ok(VmValue::List(Box::new(result)))
9437            }
9438            "reduce" => {
9439                if args.len() < 2 {
9440                    return Err(runtime_err("reduce() expects initial value and function"));
9441                }
9442                let mut acc = args[0].clone();
9443                let func = &args[1];
9444                for item in items {
9445                    acc = self.call_vm_function(func, &[acc, item])?;
9446                }
9447                Ok(acc)
9448            }
9449            "sort" => {
9450                let mut sorted = items;
9451                sorted.sort_by(|a, b| match (a, b) {
9452                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9453                    (VmValue::Float(x), VmValue::Float(y)) => {
9454                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9455                    }
9456                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9457                    _ => std::cmp::Ordering::Equal,
9458                });
9459                Ok(VmValue::List(Box::new(sorted)))
9460            }
9461            "reverse" => {
9462                let mut reversed = items;
9463                reversed.reverse();
9464                Ok(VmValue::List(Box::new(reversed)))
9465            }
9466            "contains" => {
9467                if args.is_empty() {
9468                    return Err(runtime_err("contains() expects a value"));
9469                }
9470                let needle = &args[0];
9471                let found = items.iter().any(|item| match (item, needle) {
9472                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
9473                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
9474                    (VmValue::String(a), VmValue::String(b)) => a == b,
9475                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9476                    (VmValue::None, VmValue::None) => true,
9477                    _ => false,
9478                });
9479                Ok(VmValue::Bool(found))
9480            }
9481            "index_of" => {
9482                if args.is_empty() {
9483                    return Err(runtime_err("index_of() expects a value"));
9484                }
9485                let needle = &args[0];
9486                let idx = items.iter().position(|item| match (item, needle) {
9487                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
9488                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
9489                    (VmValue::String(a), VmValue::String(b)) => a == b,
9490                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9491                    (VmValue::None, VmValue::None) => true,
9492                    _ => false,
9493                });
9494                Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
9495            }
9496            "slice" => {
9497                if args.len() < 2 {
9498                    return Err(runtime_err("slice() expects start and end"));
9499                }
9500                let start = match &args[0] {
9501                    VmValue::Int(n) => *n as usize,
9502                    _ => return Err(runtime_err("slice() expects integers")),
9503                };
9504                let end = match &args[1] {
9505                    VmValue::Int(n) => *n as usize,
9506                    _ => return Err(runtime_err("slice() expects integers")),
9507                };
9508                let end = end.min(items.len());
9509                let start = start.min(end);
9510                Ok(VmValue::List(Box::new(items[start..end].to_vec())))
9511            }
9512            "flat_map" => {
9513                if args.is_empty() {
9514                    return Err(runtime_err("flat_map() expects a function"));
9515                }
9516                let func = &args[0];
9517                let mut result = Vec::new();
9518                for item in items {
9519                    let val = self.call_vm_function(func, &[item])?;
9520                    match val {
9521                        VmValue::List(sub) => result.extend(*sub),
9522                        other => result.push(other),
9523                    }
9524                }
9525                Ok(VmValue::List(Box::new(result)))
9526            }
9527            "find" => {
9528                if args.is_empty() {
9529                    return Err(runtime_err("find() expects a predicate function"));
9530                }
9531                let func = &args[0];
9532                for item in items {
9533                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9534                    if val.is_truthy() {
9535                        return Ok(item);
9536                    }
9537                }
9538                Ok(VmValue::None)
9539            }
9540            "sort_by" => {
9541                if args.is_empty() {
9542                    return Err(runtime_err("sort_by() expects a key function"));
9543                }
9544                let func = &args[0];
9545                let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9546                for item in items {
9547                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9548                    keyed.push((key, item));
9549                }
9550                keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9551                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9552                    (VmValue::Float(x), VmValue::Float(y)) => {
9553                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9554                    }
9555                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9556                    _ => std::cmp::Ordering::Equal,
9557                });
9558                Ok(VmValue::List(Box::new(
9559                    keyed.into_iter().map(|(_, v)| v).collect(),
9560                )))
9561            }
9562            "group_by" => {
9563                if args.is_empty() {
9564                    return Err(runtime_err("group_by() expects a key function"));
9565                }
9566                let func = &args[0];
9567                let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9568                for item in items {
9569                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9570                    let key_str: Arc<str> = match &key {
9571                        VmValue::String(s) => s.clone(),
9572                        other => Arc::from(format!("{other}").as_str()),
9573                    };
9574                    if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9575                        group.1.push(item);
9576                    } else {
9577                        groups.push((key_str, vec![item]));
9578                    }
9579                }
9580                let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9581                    .into_iter()
9582                    .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9583                    .collect();
9584                Ok(VmValue::Map(Box::new(map_pairs)))
9585            }
9586            "unique" => {
9587                let mut seen = Vec::new();
9588                let mut result = Vec::new();
9589                for item in &items {
9590                    let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9591                    if !is_dup {
9592                        seen.push(item.clone());
9593                        result.push(item.clone());
9594                    }
9595                }
9596                Ok(VmValue::List(Box::new(result)))
9597            }
9598            "flatten" => {
9599                let mut result = Vec::new();
9600                for item in items {
9601                    match item {
9602                        VmValue::List(sub) => result.extend(*sub),
9603                        other => result.push(other),
9604                    }
9605                }
9606                Ok(VmValue::List(Box::new(result)))
9607            }
9608            "chunk" => {
9609                if args.is_empty() {
9610                    return Err(runtime_err("chunk() expects a size"));
9611                }
9612                let n = match &args[0] {
9613                    VmValue::Int(n) if *n > 0 => *n as usize,
9614                    _ => return Err(runtime_err("chunk() expects a positive integer")),
9615                };
9616                let chunks: Vec<VmValue> = items
9617                    .chunks(n)
9618                    .map(|c| VmValue::List(Box::new(c.to_vec())))
9619                    .collect();
9620                Ok(VmValue::List(Box::new(chunks)))
9621            }
9622            "insert" => {
9623                if args.len() < 2 {
9624                    return Err(runtime_err("insert() expects index and value"));
9625                }
9626                let idx = match &args[0] {
9627                    VmValue::Int(n) => *n as usize,
9628                    _ => return Err(runtime_err("insert() expects integer index")),
9629                };
9630                let mut new_items = items;
9631                if idx > new_items.len() {
9632                    return Err(runtime_err("insert() index out of bounds"));
9633                }
9634                new_items.insert(idx, args[1].clone());
9635                Ok(VmValue::List(Box::new(new_items)))
9636            }
9637            "remove_at" => {
9638                if args.is_empty() {
9639                    return Err(runtime_err("remove_at() expects an index"));
9640                }
9641                let idx = match &args[0] {
9642                    VmValue::Int(n) => *n as usize,
9643                    _ => return Err(runtime_err("remove_at() expects integer index")),
9644                };
9645                let mut new_items = items;
9646                if idx >= new_items.len() {
9647                    return Err(runtime_err("remove_at() index out of bounds"));
9648                }
9649                let removed = new_items.remove(idx);
9650                Ok(removed)
9651            }
9652            "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9653            "sum" => {
9654                let mut int_sum: i64 = 0;
9655                let mut has_float = false;
9656                let mut float_sum: f64 = 0.0;
9657                for item in &items {
9658                    match item {
9659                        VmValue::Int(n) => {
9660                            if has_float {
9661                                float_sum += *n as f64;
9662                            } else {
9663                                int_sum += n;
9664                            }
9665                        }
9666                        VmValue::Float(f) => {
9667                            if !has_float {
9668                                has_float = true;
9669                                float_sum = int_sum as f64;
9670                            }
9671                            float_sum += f;
9672                        }
9673                        _ => return Err(runtime_err("sum() requires numeric list")),
9674                    }
9675                }
9676                if has_float {
9677                    Ok(VmValue::Float(float_sum))
9678                } else {
9679                    Ok(VmValue::Int(int_sum))
9680                }
9681            }
9682            "min" => {
9683                if items.is_empty() {
9684                    return Ok(VmValue::None);
9685                }
9686                let mut min_val = items[0].clone();
9687                for item in &items[1..] {
9688                    match (&min_val, item) {
9689                        (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9690                        (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9691                        _ => {}
9692                    }
9693                }
9694                Ok(min_val)
9695            }
9696            "max" => {
9697                if items.is_empty() {
9698                    return Ok(VmValue::None);
9699                }
9700                let mut max_val = items[0].clone();
9701                for item in &items[1..] {
9702                    match (&max_val, item) {
9703                        (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9704                        (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9705                        _ => {}
9706                    }
9707                }
9708                Ok(max_val)
9709            }
9710            "each" => {
9711                if args.is_empty() {
9712                    return Err(runtime_err("each() expects a function"));
9713                }
9714                let func = &args[0];
9715                for item in items {
9716                    self.call_vm_function(func, &[item])?;
9717                }
9718                Ok(VmValue::None)
9719            }
9720            "zip" => {
9721                if args.is_empty() {
9722                    return Err(runtime_err("zip() expects a list"));
9723                }
9724                let other = match &args[0] {
9725                    VmValue::List(other) => other.as_slice(),
9726                    _ => return Err(runtime_err("zip() expects a list")),
9727                };
9728                let len = items.len().min(other.len());
9729                let zipped: Vec<VmValue> = items[..len]
9730                    .iter()
9731                    .zip(other[..len].iter())
9732                    .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9733                    .collect();
9734                Ok(VmValue::List(Box::new(zipped)))
9735            }
9736            "join" => {
9737                let sep = match args.first() {
9738                    Some(VmValue::String(s)) => s.as_ref(),
9739                    _ => "",
9740                };
9741                let s: String = items
9742                    .iter()
9743                    .map(|v| format!("{v}"))
9744                    .collect::<Vec<_>>()
9745                    .join(sep);
9746                Ok(VmValue::String(Arc::from(s.as_str())))
9747            }
9748            _ => Err(runtime_err(format!("No method '{}' on list", method))),
9749        }
9750    }
9751
9752    /// Dispatch map methods.
9753    fn dispatch_map_method(
9754        &mut self,
9755        pairs: Vec<(Arc<str>, VmValue)>,
9756        method: &str,
9757        args: &[VmValue],
9758    ) -> Result<VmValue, TlError> {
9759        match method {
9760            "len" => Ok(VmValue::Int(pairs.len() as i64)),
9761            "keys" => Ok(VmValue::List(Box::new(
9762                pairs
9763                    .iter()
9764                    .map(|(k, _)| VmValue::String(k.clone()))
9765                    .collect(),
9766            ))),
9767            "values" => Ok(VmValue::List(Box::new(
9768                pairs.iter().map(|(_, v)| v.clone()).collect(),
9769            ))),
9770            "contains_key" => {
9771                if args.is_empty() {
9772                    return Err(runtime_err("contains_key() expects a key"));
9773                }
9774                if let VmValue::String(key) = &args[0] {
9775                    Ok(VmValue::Bool(
9776                        pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9777                    ))
9778                } else {
9779                    Err(runtime_err("contains_key() expects a string key"))
9780                }
9781            }
9782            "remove" => {
9783                if args.is_empty() {
9784                    return Err(runtime_err("remove() expects a key"));
9785                }
9786                if let VmValue::String(key) = &args[0] {
9787                    let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9788                        .into_iter()
9789                        .filter(|(k, _)| k.as_ref() != key.as_ref())
9790                        .collect();
9791                    Ok(VmValue::Map(Box::new(new_pairs)))
9792                } else {
9793                    Err(runtime_err("remove() expects a string key"))
9794                }
9795            }
9796            "get" => {
9797                if args.is_empty() {
9798                    return Err(runtime_err("get() expects a key"));
9799                }
9800                if let VmValue::String(key) = &args[0] {
9801                    let default = args.get(1).cloned().unwrap_or(VmValue::None);
9802                    let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9803                    Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9804                } else {
9805                    Err(runtime_err("get() expects a string key"))
9806                }
9807            }
9808            "merge" => {
9809                if args.is_empty() {
9810                    return Err(runtime_err("merge() expects a map"));
9811                }
9812                if let VmValue::Map(other) = &args[0] {
9813                    let mut merged = pairs;
9814                    for (k, v) in other.iter() {
9815                        if let Some(existing) =
9816                            merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9817                        {
9818                            existing.1 = v.clone();
9819                        } else {
9820                            merged.push((k.clone(), v.clone()));
9821                        }
9822                    }
9823                    Ok(VmValue::Map(Box::new(merged)))
9824                } else {
9825                    Err(runtime_err("merge() expects a map"))
9826                }
9827            }
9828            "entries" => {
9829                let entries: Vec<VmValue> = pairs
9830                    .iter()
9831                    .map(|(k, v)| {
9832                        VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9833                    })
9834                    .collect();
9835                Ok(VmValue::List(Box::new(entries)))
9836            }
9837            "map_values" => {
9838                if args.is_empty() {
9839                    return Err(runtime_err("map_values() expects a function"));
9840                }
9841                let func = &args[0];
9842                let mut result = Vec::new();
9843                for (k, v) in pairs {
9844                    let new_v = self.call_vm_function(func, &[v])?;
9845                    result.push((k, new_v));
9846                }
9847                Ok(VmValue::Map(Box::new(result)))
9848            }
9849            "filter" => {
9850                if args.is_empty() {
9851                    return Err(runtime_err("filter() expects a predicate function"));
9852                }
9853                let func = &args[0];
9854                let mut result = Vec::new();
9855                for (k, v) in pairs {
9856                    let val =
9857                        self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9858                    if val.is_truthy() {
9859                        result.push((k, v));
9860                    }
9861                }
9862                Ok(VmValue::Map(Box::new(result)))
9863            }
9864            "set" => {
9865                if args.len() < 2 {
9866                    return Err(runtime_err("set() expects key and value"));
9867                }
9868                if let VmValue::String(key) = &args[0] {
9869                    let mut new_pairs = pairs;
9870                    if let Some(existing) = new_pairs
9871                        .iter_mut()
9872                        .find(|(k, _)| k.as_ref() == key.as_ref())
9873                    {
9874                        existing.1 = args[1].clone();
9875                    } else {
9876                        new_pairs.push((key.clone(), args[1].clone()));
9877                    }
9878                    Ok(VmValue::Map(Box::new(new_pairs)))
9879                } else {
9880                    Err(runtime_err("set() expects a string key"))
9881                }
9882            }
9883            "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9884            _ => Err(runtime_err(format!("No method '{}' on map", method))),
9885        }
9886    }
9887
9888    /// Dispatch set methods.
9889    fn dispatch_set_method(
9890        &self,
9891        items: Vec<VmValue>,
9892        method: &str,
9893        args: &[VmValue],
9894    ) -> Result<VmValue, TlError> {
9895        match method {
9896            "len" => Ok(VmValue::Int(items.len() as i64)),
9897            "contains" => {
9898                if args.is_empty() {
9899                    return Err(runtime_err("contains() expects a value"));
9900                }
9901                Ok(VmValue::Bool(
9902                    items.iter().any(|x| vm_values_equal(x, &args[0])),
9903                ))
9904            }
9905            "add" => {
9906                if args.is_empty() {
9907                    return Err(runtime_err("add() expects a value"));
9908                }
9909                let mut new_items = items;
9910                if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9911                    new_items.push(args[0].clone());
9912                }
9913                Ok(VmValue::Set(Box::new(new_items)))
9914            }
9915            "remove" => {
9916                if args.is_empty() {
9917                    return Err(runtime_err("remove() expects a value"));
9918                }
9919                let new_items: Vec<VmValue> = items
9920                    .into_iter()
9921                    .filter(|x| !vm_values_equal(x, &args[0]))
9922                    .collect();
9923                Ok(VmValue::Set(Box::new(new_items)))
9924            }
9925            "to_list" => Ok(VmValue::List(Box::new(items))),
9926            "union" => {
9927                if args.is_empty() {
9928                    return Err(runtime_err("union() expects a set"));
9929                }
9930                if let VmValue::Set(b) = &args[0] {
9931                    let mut result = items;
9932                    for item in b.iter() {
9933                        if !result.iter().any(|x| vm_values_equal(x, item)) {
9934                            result.push(item.clone());
9935                        }
9936                    }
9937                    Ok(VmValue::Set(Box::new(result)))
9938                } else {
9939                    Err(runtime_err("union() expects a set"))
9940                }
9941            }
9942            "intersection" => {
9943                if args.is_empty() {
9944                    return Err(runtime_err("intersection() expects a set"));
9945                }
9946                if let VmValue::Set(b) = &args[0] {
9947                    let result: Vec<VmValue> = items
9948                        .into_iter()
9949                        .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9950                        .collect();
9951                    Ok(VmValue::Set(Box::new(result)))
9952                } else {
9953                    Err(runtime_err("intersection() expects a set"))
9954                }
9955            }
9956            "difference" => {
9957                if args.is_empty() {
9958                    return Err(runtime_err("difference() expects a set"));
9959                }
9960                if let VmValue::Set(b) = &args[0] {
9961                    let result: Vec<VmValue> = items
9962                        .into_iter()
9963                        .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9964                        .collect();
9965                    Ok(VmValue::Set(Box::new(result)))
9966                } else {
9967                    Err(runtime_err("difference() expects a set"))
9968                }
9969            }
9970            _ => Err(runtime_err(format!("No method '{}' on set", method))),
9971        }
9972    }
9973
9974    /// Handle import at runtime.
9975    #[cfg(feature = "native")]
9976    fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9977        // Resolve relative path from current file
9978        let resolved = if let Some(ref base) = self.file_path {
9979            let base_dir = std::path::Path::new(base)
9980                .parent()
9981                .unwrap_or(std::path::Path::new("."));
9982            let candidate = base_dir.join(path);
9983            if candidate.exists() {
9984                candidate.to_string_lossy().to_string()
9985            } else {
9986                path.to_string()
9987            }
9988        } else {
9989            path.to_string()
9990        };
9991
9992        // Circular dependency detection
9993        if self.importing_files.contains(&resolved) {
9994            return Err(runtime_err(format!("Circular import detected: {resolved}")));
9995        }
9996
9997        // Check module cache
9998        if let Some(exports) = self.module_cache.get(&resolved) {
9999            let exports = exports.clone();
10000            return self.bind_import_exports(exports, alias);
10001        }
10002
10003        // Read, parse, compile, execute the file
10004        let source = std::fs::read_to_string(&resolved)
10005            .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
10006        let program = tl_parser::parse(&source)
10007            .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
10008        let proto = crate::compiler::compile(&program)
10009            .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
10010
10011        // Track circular imports
10012        self.importing_files.insert(resolved.clone());
10013
10014        // Execute in a fresh VM with shared globals
10015        let mut import_vm = Vm::new();
10016        import_vm.file_path = Some(resolved.clone());
10017        import_vm.globals = self.globals.clone();
10018        import_vm.importing_files = self.importing_files.clone();
10019        import_vm.module_cache = self.module_cache.clone();
10020        import_vm.package_roots = self.package_roots.clone();
10021        import_vm.project_root = self.project_root.clone();
10022        import_vm.execute(&proto)?;
10023
10024        self.importing_files.remove(&resolved);
10025
10026        // Collect exports: both globals and top-level locals from the stack
10027        let mut exports = HashMap::new();
10028
10029        // 1. New globals defined in the import
10030        for (k, v) in &import_vm.globals {
10031            if !self.globals.contains_key(k) {
10032                exports.insert(k.clone(), v.clone());
10033            }
10034        }
10035
10036        // 2. Top-level locals from the prototype (on the stack)
10037        for (name, reg) in &proto.top_level_locals {
10038            if !name.starts_with("__enum_") && !exports.contains_key(name) {
10039                let stack_idx = reg;
10040                if (*stack_idx as usize) < import_vm.stack.len() {
10041                    let val = import_vm.stack[*stack_idx as usize].clone();
10042                    if !matches!(val, VmValue::None) || name.starts_with("_") {
10043                        exports.insert(name.clone(), val);
10044                    }
10045                }
10046            }
10047        }
10048
10049        // Cache the module
10050        self.module_cache.insert(resolved, exports.clone());
10051        // Also adopt any modules the sub-VM discovered
10052        for (k, v) in import_vm.module_cache {
10053            self.module_cache.entry(k).or_insert(v);
10054        }
10055
10056        self.bind_import_exports(exports, alias)
10057    }
10058
10059    /// Bind import exports into current scope.
10060    #[cfg(feature = "native")]
10061    fn bind_import_exports(
10062        &mut self,
10063        exports: HashMap<String, VmValue>,
10064        alias: &str,
10065    ) -> Result<VmValue, TlError> {
10066        if alias.is_empty() {
10067            // Wildcard import: merge all exports into current scope
10068            for (k, v) in &exports {
10069                self.globals.insert(k.clone(), v.clone());
10070            }
10071            Ok(VmValue::None)
10072        } else {
10073            // Namespaced import
10074            let module = VmModule {
10075                name: Arc::from(alias),
10076                exports,
10077            };
10078            let module_val = VmValue::Module(Arc::new(module));
10079            self.globals.insert(alias.to_string(), module_val.clone());
10080            Ok(module_val)
10081        }
10082    }
10083
10084    /// Handle use-style imports (dot-path syntax).
10085    #[cfg(feature = "native")]
10086    fn handle_use_import(
10087        &mut self,
10088        path_str: &str,
10089        extra_a: u8,
10090        kind: u8,
10091        _frame_idx: usize,
10092    ) -> Result<VmValue, TlError> {
10093        match kind {
10094            0 => {
10095                // Single: "data.transforms.clean" → import file, bind last segment
10096                let segments: Vec<&str> = path_str.split('.').collect();
10097                let file_path = self.resolve_use_path(&segments)?;
10098                // Import the module, get exports
10099                let _last = segments.last().copied().unwrap_or("");
10100                self.handle_import(&file_path, "")?;
10101                // The wildcard import already merged everything.
10102                // But for Single, we only want the specific item.
10103                // Since handle_import merges all, that works for now.
10104                // Return none since it's a statement, not an expression.
10105                Ok(VmValue::None)
10106            }
10107            1 => {
10108                // Group: "data.transforms.{a,b}" — extract prefix before {
10109                let brace_start = path_str.find('{').unwrap_or(path_str.len());
10110                let prefix = path_str[..brace_start].trim_end_matches('.');
10111                let segments: Vec<&str> = prefix.split('.').collect();
10112                let file_path = self.resolve_use_path(&segments)?;
10113                self.handle_import(&file_path, "")?;
10114                Ok(VmValue::None)
10115            }
10116            2 => {
10117                // Wildcard: "data.transforms.*" — strip trailing .*
10118                let prefix = path_str.trim_end_matches(".*");
10119                let segments: Vec<&str> = prefix.split('.').collect();
10120                let file_path = self.resolve_use_path(&segments)?;
10121                self.handle_import(&file_path, "")?;
10122                Ok(VmValue::None)
10123            }
10124            3 => {
10125                // Aliased: path in path_str, alias in extra_a (constant index)
10126                let segments: Vec<&str> = path_str.split('.').collect();
10127                let file_path = self.resolve_use_path(&segments)?;
10128                // For aliased, we need to get the alias from the constant pool
10129                // extra_a contains the constant index of the alias string
10130                let alias_str = if let Some(frame) = self.frames.last() {
10131                    if let Some(crate::chunk::Constant::String(s)) =
10132                        frame.prototype.constants.get(extra_a as usize)
10133                    {
10134                        s.to_string()
10135                    } else {
10136                        segments.last().copied().unwrap_or("module").to_string()
10137                    }
10138                } else {
10139                    segments.last().copied().unwrap_or("module").to_string()
10140                };
10141                self.handle_import(&file_path, &alias_str)?;
10142                Ok(VmValue::None)
10143            }
10144            _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
10145        }
10146    }
10147
10148    /// Resolve dot-path segments to a file path for use statements.
10149    #[cfg(feature = "native")]
10150    fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
10151        // Reject path traversal attempts
10152        if segments.contains(&"..") {
10153            return Err(runtime_err("Import paths cannot contain '..'"));
10154        }
10155
10156        let base_dir = if let Some(ref fp) = self.file_path {
10157            std::path::Path::new(fp)
10158                .parent()
10159                .unwrap_or(std::path::Path::new("."))
10160                .to_path_buf()
10161        } else {
10162            std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
10163        };
10164
10165        let rel_path = segments.join("/");
10166
10167        // Try file module first
10168        let file_path = base_dir.join(format!("{rel_path}.tl"));
10169        if file_path.exists() {
10170            return Ok(file_path.to_string_lossy().to_string());
10171        }
10172
10173        // Try directory module
10174        let dir_path = base_dir.join(&rel_path).join("mod.tl");
10175        if dir_path.exists() {
10176            return Ok(dir_path.to_string_lossy().to_string());
10177        }
10178
10179        // If multi-segment, try parent as file module
10180        if segments.len() > 1 {
10181            let parent = &segments[..segments.len() - 1];
10182            let parent_path = parent.join("/");
10183            let parent_file = base_dir.join(format!("{parent_path}.tl"));
10184            if parent_file.exists() {
10185                return Ok(parent_file.to_string_lossy().to_string());
10186            }
10187            let parent_dir = base_dir.join(&parent_path).join("mod.tl");
10188            if parent_dir.exists() {
10189                return Ok(parent_dir.to_string_lossy().to_string());
10190            }
10191        }
10192
10193        // Package import fallback: first segment as package name
10194        // Convert underscores to hyphens (TL identifiers use _, package names use -)
10195        let pkg_name_underscore = segments[0];
10196        let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
10197        let pkg_root = self
10198            .package_roots
10199            .get(pkg_name_underscore)
10200            .or_else(|| self.package_roots.get(&pkg_name_hyphen));
10201
10202        if let Some(root) = pkg_root {
10203            let remaining = &segments[1..];
10204            if let Some(path) = resolve_package_file(root, remaining) {
10205                return Ok(path);
10206            }
10207        }
10208
10209        Err(runtime_err(format!(
10210            "Module not found: `{}`",
10211            segments.join(".")
10212        )))
10213    }
10214
10215    /// Call a VmValue function/closure with args.
10216    fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
10217        match func {
10218            VmValue::Function(closure) => {
10219                let proto = closure.prototype.clone();
10220                let arity = proto.arity as usize;
10221                if args.len() != arity {
10222                    return Err(runtime_err(format!(
10223                        "Expected {} arguments, got {}",
10224                        arity,
10225                        args.len()
10226                    )));
10227                }
10228
10229                // If this is a generator function, create a Generator
10230                if proto.is_generator {
10231                    let mut closed_upvalues = Vec::new();
10232                    for uv in &closure.upvalues {
10233                        match uv {
10234                            UpvalueRef::Open { stack_index } => {
10235                                let val = self.stack[*stack_index].clone();
10236                                closed_upvalues.push(UpvalueRef::Closed(val));
10237                            }
10238                            UpvalueRef::Closed(v) => {
10239                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
10240                            }
10241                        }
10242                    }
10243                    let num_regs = proto.num_registers as usize;
10244                    let mut saved_stack = vec![VmValue::None; num_regs];
10245                    for (i, arg) in args.iter().enumerate() {
10246                        saved_stack[i] = arg.clone();
10247                    }
10248                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
10249                        prototype: proto,
10250                        upvalues: closed_upvalues,
10251                        saved_stack,
10252                        ip: 0,
10253                    });
10254                    return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
10255                }
10256
10257                let new_base = self.stack.len();
10258                self.ensure_stack(new_base + proto.num_registers as usize + 1);
10259
10260                for (i, arg) in args.iter().enumerate() {
10261                    self.stack[new_base + i] = arg.clone();
10262                }
10263
10264                self.frames.push(CallFrame {
10265                    prototype: proto,
10266                    ip: 0,
10267                    base: new_base,
10268                    upvalues: closure.upvalues.clone(),
10269                });
10270
10271                let result = self.run()?;
10272                self.stack.truncate(new_base);
10273                Ok(result)
10274            }
10275            VmValue::Builtin(id) => {
10276                // Put args on stack temporarily
10277                let args_base = self.stack.len();
10278                for arg in args {
10279                    self.stack.push(arg.clone());
10280                }
10281                let result = self.call_builtin(*id as u16, args_base, args.len());
10282                self.stack.truncate(args_base);
10283                result
10284            }
10285            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
10286        }
10287    }
10288
10289    // ── Table pipe handler ──
10290
10291    #[cfg(feature = "native")]
10292    fn handle_table_pipe(
10293        &mut self,
10294        frame_idx: usize,
10295        table_val: VmValue,
10296        op_const: u8,
10297        args_const: u8,
10298    ) -> Result<VmValue, TlError> {
10299        let df = match table_val {
10300            VmValue::Table(t) => t.df,
10301            other => {
10302                // Not a table — fall back to regular builtin/function call
10303                return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
10304            }
10305        };
10306
10307        let frame = &self.frames[frame_idx];
10308        let op_name = match &frame.prototype.constants[op_const as usize] {
10309            Constant::String(s) => s.to_string(),
10310            _ => return Err(runtime_err("Expected string constant for table op")),
10311        };
10312        let ast_args = match &frame.prototype.constants[args_const as usize] {
10313            Constant::AstExprList(args) => args.clone(),
10314            _ => return Err(runtime_err("Expected AST expr list for table args")),
10315        };
10316
10317        let ctx = self.build_translate_context();
10318
10319        match op_name.as_str() {
10320            "filter" => {
10321                if ast_args.len() != 1 {
10322                    return Err(runtime_err("filter() expects 1 argument (predicate)"));
10323                }
10324                let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
10325                let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
10326                Ok(VmValue::Table(VmTable { df: filtered }))
10327            }
10328            "select" => {
10329                if ast_args.is_empty() {
10330                    return Err(runtime_err("select() expects at least 1 argument"));
10331                }
10332                let mut select_exprs = Vec::new();
10333                for arg in &ast_args {
10334                    match arg {
10335                        AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
10336                        AstExpr::NamedArg { name, value } => {
10337                            let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10338                            select_exprs.push(expr.alias(name));
10339                        }
10340                        AstExpr::String(name) => select_exprs.push(col(name.as_str())),
10341                        other => {
10342                            let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10343                            select_exprs.push(expr);
10344                        }
10345                    }
10346                }
10347                let selected = df
10348                    .select(select_exprs)
10349                    .map_err(|e| runtime_err(format!("{e}")))?;
10350                Ok(VmValue::Table(VmTable { df: selected }))
10351            }
10352            "sort" => {
10353                if ast_args.is_empty() {
10354                    return Err(runtime_err("sort() expects at least 1 argument (column)"));
10355                }
10356                let mut sort_exprs = Vec::new();
10357                let mut i = 0;
10358                while i < ast_args.len() {
10359                    let col_name = match &ast_args[i] {
10360                        AstExpr::Ident(name) => name.clone(),
10361                        AstExpr::String(name) => name.clone(),
10362                        _ => {
10363                            return Err(runtime_err(
10364                                "sort() column must be an identifier or string",
10365                            ));
10366                        }
10367                    };
10368                    i += 1;
10369                    let ascending = if i < ast_args.len() {
10370                        match &ast_args[i] {
10371                            AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
10372                                i += 1;
10373                                false
10374                            }
10375                            AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
10376                                i += 1;
10377                                true
10378                            }
10379                            _ => true,
10380                        }
10381                    } else {
10382                        true
10383                    };
10384                    sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
10385                }
10386                let sorted = df
10387                    .sort(sort_exprs)
10388                    .map_err(|e| runtime_err(format!("{e}")))?;
10389                Ok(VmValue::Table(VmTable { df: sorted }))
10390            }
10391            "with" => {
10392                if ast_args.len() != 1 {
10393                    return Err(runtime_err(
10394                        "with() expects 1 argument (map of column definitions)",
10395                    ));
10396                }
10397                let pairs = match &ast_args[0] {
10398                    AstExpr::Map(pairs) => pairs,
10399                    _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
10400                };
10401                let mut result_df = df;
10402                for (key, value_expr) in pairs {
10403                    let col_name = match key {
10404                        AstExpr::String(s) => s.clone(),
10405                        AstExpr::Ident(s) => s.clone(),
10406                        _ => return Err(runtime_err("with() key must be a string or identifier")),
10407                    };
10408                    let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
10409                    result_df = result_df
10410                        .with_column(&col_name, df_expr)
10411                        .map_err(|e| runtime_err(format!("{e}")))?;
10412                }
10413                Ok(VmValue::Table(VmTable { df: result_df }))
10414            }
10415            "aggregate" => {
10416                let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10417                let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10418                for arg in &ast_args {
10419                    match arg {
10420                        AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
10421                            AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
10422                            AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
10423                            AstExpr::List(items) => {
10424                                for item in items {
10425                                    match item {
10426                                        AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
10427                                        AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
10428                                        _ => {
10429                                            return Err(runtime_err(
10430                                                "by: list items must be strings or identifiers",
10431                                            ));
10432                                        }
10433                                    }
10434                                }
10435                            }
10436                            _ => return Err(runtime_err("by: must be a column name or list")),
10437                        },
10438                        AstExpr::NamedArg { name, value } => {
10439                            let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10440                            agg_exprs.push(agg_expr.alias(name));
10441                        }
10442                        other => {
10443                            let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10444                            agg_exprs.push(agg_expr);
10445                        }
10446                    }
10447                }
10448                let aggregated = df
10449                    .aggregate(group_by_cols, agg_exprs)
10450                    .map_err(|e| runtime_err(format!("{e}")))?;
10451                Ok(VmValue::Table(VmTable { df: aggregated }))
10452            }
10453            "join" => {
10454                if ast_args.is_empty() {
10455                    return Err(runtime_err(
10456                        "join() expects at least 1 argument (right table)",
10457                    ));
10458                }
10459                // Evaluate first arg to get right table
10460                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10461                let right_df = match right_table {
10462                    VmValue::Table(t) => t.df,
10463                    _ => return Err(runtime_err("join() first arg must be a table")),
10464                };
10465                let mut left_cols: Vec<String> = Vec::new();
10466                let mut right_cols: Vec<String> = Vec::new();
10467                let mut join_type = JoinType::Inner;
10468                for arg in &ast_args[1..] {
10469                    match arg {
10470                        AstExpr::NamedArg { name, value } if name == "on" => {
10471                            if let AstExpr::BinOp {
10472                                left,
10473                                op: tl_ast::BinOp::Eq,
10474                                right,
10475                            } = value.as_ref()
10476                            {
10477                                let lc = match left.as_ref() {
10478                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10479                                    _ => {
10480                                        return Err(runtime_err(
10481                                            "on: left side must be a column name",
10482                                        ));
10483                                    }
10484                                };
10485                                let rc = match right.as_ref() {
10486                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10487                                    _ => {
10488                                        return Err(runtime_err(
10489                                            "on: right side must be a column name",
10490                                        ));
10491                                    }
10492                                };
10493                                left_cols.push(lc);
10494                                right_cols.push(rc);
10495                            }
10496                        }
10497                        AstExpr::NamedArg { name, value } if name == "kind" => {
10498                            if let AstExpr::String(kind_str) = value.as_ref() {
10499                                join_type = match kind_str.as_str() {
10500                                    "inner" => JoinType::Inner,
10501                                    "left" => JoinType::Left,
10502                                    "right" => JoinType::Right,
10503                                    "full" => JoinType::Full,
10504                                    _ => {
10505                                        return Err(runtime_err(format!(
10506                                            "Unknown join type: {kind_str}"
10507                                        )));
10508                                    }
10509                                };
10510                            }
10511                        }
10512                        _ => {}
10513                    }
10514                }
10515                let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
10516                let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
10517                let joined = df
10518                    .join(right_df, join_type, &lc_refs, &rc_refs, None)
10519                    .map_err(|e| runtime_err(format!("{e}")))?;
10520                Ok(VmValue::Table(VmTable { df: joined }))
10521            }
10522            "head" | "limit" => {
10523                let n = match ast_args.first() {
10524                    Some(AstExpr::Int(n)) => *n as usize,
10525                    None => 10,
10526                    _ => return Err(runtime_err("head/limit expects an integer")),
10527                };
10528                let limited = df
10529                    .limit(0, Some(n))
10530                    .map_err(|e| runtime_err(format!("{e}")))?;
10531                Ok(VmValue::Table(VmTable { df: limited }))
10532            }
10533            "collect" => {
10534                let batches = self.engine().collect(df).map_err(runtime_err)?;
10535                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10536                Ok(VmValue::String(Arc::from(formatted.as_str())))
10537            }
10538            "show" => {
10539                let limit = match ast_args.first() {
10540                    Some(AstExpr::Int(n)) => *n as usize,
10541                    None => 20,
10542                    _ => 20,
10543                };
10544                let limited = df
10545                    .limit(0, Some(limit))
10546                    .map_err(|e| runtime_err(format!("{e}")))?;
10547                let batches = self.engine().collect(limited).map_err(runtime_err)?;
10548                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10549                println!("{formatted}");
10550                self.output.push(formatted);
10551                Ok(VmValue::None)
10552            }
10553            "describe" => {
10554                let schema = df.schema();
10555                let mut lines = Vec::new();
10556                lines.push("Columns:".to_string());
10557                for field in schema.fields() {
10558                    lines.push(format!("  {}: {}", field.name(), field.data_type()));
10559                }
10560                let output = lines.join("\n");
10561                println!("{output}");
10562                self.output.push(output.clone());
10563                Ok(VmValue::String(Arc::from(output.as_str())))
10564            }
10565            "write_csv" => {
10566                if ast_args.len() != 1 {
10567                    return Err(runtime_err("write_csv() expects 1 argument (path)"));
10568                }
10569                let path = self.eval_ast_to_string(&ast_args[0])?;
10570                self.engine().write_csv(df, &path).map_err(runtime_err)?;
10571                Ok(VmValue::None)
10572            }
10573            "write_parquet" => {
10574                if ast_args.len() != 1 {
10575                    return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10576                }
10577                let path = self.eval_ast_to_string(&ast_args[0])?;
10578                self.engine()
10579                    .write_parquet(df, &path)
10580                    .map_err(runtime_err)?;
10581                Ok(VmValue::None)
10582            }
10583            // Phase 15: Data quality pipe operations
10584            "fill_null" => {
10585                if ast_args.is_empty() {
10586                    return Err(runtime_err(
10587                        "fill_null() expects (column, [strategy/value])",
10588                    ));
10589                }
10590                let column = self.eval_ast_to_string(&ast_args[0])?;
10591                if ast_args.len() >= 2 {
10592                    let val = self.eval_ast_to_vm(&ast_args[1])?;
10593                    match val {
10594                        VmValue::String(s) => {
10595                            // String means strategy name
10596                            let fill_val = if ast_args.len() >= 3 {
10597                                match self.eval_ast_to_vm(&ast_args[2])? {
10598                                    VmValue::Int(n) => Some(n as f64),
10599                                    VmValue::Float(f) => Some(f),
10600                                    _ => None,
10601                                }
10602                            } else {
10603                                None
10604                            };
10605                            let result = self
10606                                .engine()
10607                                .fill_null(df, &column, &s, fill_val)
10608                                .map_err(runtime_err)?;
10609                            Ok(VmValue::Table(VmTable { df: result }))
10610                        }
10611                        VmValue::Int(n) => {
10612                            let result = self
10613                                .engine()
10614                                .fill_null(df, &column, "value", Some(n as f64))
10615                                .map_err(runtime_err)?;
10616                            Ok(VmValue::Table(VmTable { df: result }))
10617                        }
10618                        VmValue::Float(f) => {
10619                            let result = self
10620                                .engine()
10621                                .fill_null(df, &column, "value", Some(f))
10622                                .map_err(runtime_err)?;
10623                            Ok(VmValue::Table(VmTable { df: result }))
10624                        }
10625                        _ => Err(runtime_err(
10626                            "fill_null() second arg must be a strategy or fill value",
10627                        )),
10628                    }
10629                } else {
10630                    let result = self
10631                        .engine()
10632                        .fill_null(df, &column, "zero", None)
10633                        .map_err(runtime_err)?;
10634                    Ok(VmValue::Table(VmTable { df: result }))
10635                }
10636            }
10637            "drop_null" => {
10638                if ast_args.is_empty() {
10639                    return Err(runtime_err("drop_null() expects (column)"));
10640                }
10641                let column = self.eval_ast_to_string(&ast_args[0])?;
10642                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10643                Ok(VmValue::Table(VmTable { df: result }))
10644            }
10645            "dedup" => {
10646                let columns: Vec<String> = ast_args
10647                    .iter()
10648                    .filter_map(|a| self.eval_ast_to_string(a).ok())
10649                    .collect();
10650                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10651                Ok(VmValue::Table(VmTable { df: result }))
10652            }
10653            "clamp" => {
10654                if ast_args.len() < 3 {
10655                    return Err(runtime_err("clamp() expects (column, min, max)"));
10656                }
10657                let column = self.eval_ast_to_string(&ast_args[0])?;
10658                let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10659                    VmValue::Int(n) => n as f64,
10660                    VmValue::Float(f) => f,
10661                    _ => return Err(runtime_err("clamp() min must be a number")),
10662                };
10663                let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10664                    VmValue::Int(n) => n as f64,
10665                    VmValue::Float(f) => f,
10666                    _ => return Err(runtime_err("clamp() max must be a number")),
10667                };
10668                let result = self
10669                    .engine()
10670                    .clamp(df, &column, min_val, max_val)
10671                    .map_err(runtime_err)?;
10672                Ok(VmValue::Table(VmTable { df: result }))
10673            }
10674            "data_profile" => {
10675                let result = self.engine().data_profile(df).map_err(runtime_err)?;
10676                Ok(VmValue::Table(VmTable { df: result }))
10677            }
10678            "row_count" => {
10679                let count = self.engine().row_count(df).map_err(runtime_err)?;
10680                Ok(VmValue::Int(count))
10681            }
10682            "null_rate" => {
10683                if ast_args.is_empty() {
10684                    return Err(runtime_err("null_rate() expects (column)"));
10685                }
10686                let column = self.eval_ast_to_string(&ast_args[0])?;
10687                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10688                Ok(VmValue::Float(rate))
10689            }
10690            "is_unique" => {
10691                if ast_args.is_empty() {
10692                    return Err(runtime_err("is_unique() expects (column)"));
10693                }
10694                let column = self.eval_ast_to_string(&ast_args[0])?;
10695                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10696                Ok(VmValue::Bool(unique))
10697            }
10698            // Phase F2: Window functions
10699            "window" => {
10700                use tl_data::datafusion::logical_expr::{
10701                    WindowFrame, WindowFunctionDefinition,
10702                    expr::{Sort as DfSort, WindowFunction as WinFunc},
10703                };
10704                if ast_args.is_empty() {
10705                    return Err(runtime_err(
10706                        "window() expects named arguments: fn, partition_by, order_by, alias",
10707                    ));
10708                }
10709                let mut win_fn_name = String::new();
10710                let mut partition_by_cols: Vec<String> = Vec::new();
10711                let mut order_by_cols: Vec<String> = Vec::new();
10712                let mut alias_name = String::new();
10713                let mut win_args: Vec<String> = Vec::new();
10714                let mut descending = false;
10715
10716                for arg in &ast_args {
10717                    if let AstExpr::NamedArg { name, value } = arg {
10718                        match name.as_str() {
10719                            "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10720                            "partition_by" => match value.as_ref() {
10721                                AstExpr::List(items) => {
10722                                    for item in items {
10723                                        partition_by_cols.push(self.eval_ast_to_string(item)?);
10724                                    }
10725                                }
10726                                _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10727                            },
10728                            "order_by" => match value.as_ref() {
10729                                AstExpr::List(items) => {
10730                                    for item in items {
10731                                        order_by_cols.push(self.eval_ast_to_string(item)?);
10732                                    }
10733                                }
10734                                _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10735                            },
10736                            "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10737                            "args" => match value.as_ref() {
10738                                AstExpr::List(items) => {
10739                                    for item in items {
10740                                        win_args.push(self.eval_ast_to_string(item)?);
10741                                    }
10742                                }
10743                                _ => win_args.push(self.eval_ast_to_string(value)?),
10744                            },
10745                            "desc" => {
10746                                if let AstExpr::Bool(b) = value.as_ref() {
10747                                    descending = *b;
10748                                }
10749                            }
10750                            _ => {}
10751                        }
10752                    }
10753                }
10754
10755                if win_fn_name.is_empty() {
10756                    return Err(runtime_err(
10757                        "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10758                    ));
10759                }
10760                if alias_name.is_empty() {
10761                    alias_name = win_fn_name.clone();
10762                }
10763
10764                // Build window function definition
10765                let session = self.engine().session_ctx();
10766                let win_udf = match win_fn_name.as_str() {
10767                    "rank" => session.udwf("rank"),
10768                    "dense_rank" => session.udwf("dense_rank"),
10769                    "row_number" => session.udwf("row_number"),
10770                    "percent_rank" => session.udwf("percent_rank"),
10771                    "cume_dist" => session.udwf("cume_dist"),
10772                    "ntile" => session.udwf("ntile"),
10773                    "lag" => session.udwf("lag"),
10774                    "lead" => session.udwf("lead"),
10775                    "first_value" => session.udwf("first_value"),
10776                    "last_value" => session.udwf("last_value"),
10777                    _ => {
10778                        return Err(runtime_err(format!(
10779                            "Unknown window function: {win_fn_name}"
10780                        )));
10781                    }
10782                }
10783                .map_err(|e| {
10784                    runtime_err(format!(
10785                        "Window function '{win_fn_name}' not available: {e}"
10786                    ))
10787                })?;
10788
10789                let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10790
10791                // Build function args (for lag/lead/ntile)
10792                let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10793                    .iter()
10794                    .map(|a| {
10795                        if let Ok(n) = a.parse::<i64>() {
10796                            lit(n)
10797                        } else {
10798                            col(a.as_str())
10799                        }
10800                    })
10801                    .collect();
10802
10803                let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10804                    partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10805                let order_exprs: Vec<DfSort> = order_by_cols
10806                    .iter()
10807                    .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10808                    .collect();
10809
10810                let has_order = !order_exprs.is_empty();
10811                let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10812                    fun,
10813                    args: func_args,
10814                    partition_by: partition_exprs,
10815                    order_by: order_exprs,
10816                    window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10817                    null_treatment: None,
10818                })
10819                .alias(&alias_name);
10820
10821                // Get all existing columns and add the window column
10822                let schema = df.schema();
10823                let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10824                    .fields()
10825                    .iter()
10826                    .map(|f| col(f.name().as_str()))
10827                    .collect();
10828                select_exprs.push(win_expr);
10829
10830                let result_df = df
10831                    .select(select_exprs)
10832                    .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10833                Ok(VmValue::Table(VmTable { df: result_df }))
10834            }
10835            // Phase F3: Union
10836            "union" => {
10837                if ast_args.is_empty() {
10838                    return Err(runtime_err("union() expects a table argument"));
10839                }
10840                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10841                let right_df = match right_table {
10842                    VmValue::Table(t) => t.df,
10843                    _ => return Err(runtime_err("union() argument must be a table")),
10844                };
10845                let result_df = df
10846                    .union(right_df)
10847                    .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10848                Ok(VmValue::Table(VmTable { df: result_df }))
10849            }
10850            // Phase F4: Table sampling
10851            "sample" => {
10852                use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10853                use tl_data::datafusion::datasource::MemTable;
10854                if ast_args.is_empty() {
10855                    return Err(runtime_err("sample() expects a count or fraction"));
10856                }
10857                let batches = self.engine().collect(df).map_err(runtime_err)?;
10858                let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10859                let sample_count = match &ast_args[0] {
10860                    AstExpr::Int(n) => (*n as usize).min(total_rows),
10861                    AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10862                        ((total_rows as f64) * f).ceil() as usize
10863                    }
10864                    _ => {
10865                        let val = self.eval_ast_to_string(&ast_args[0])?;
10866                        val.parse::<usize>().map_err(|_| {
10867                            runtime_err("sample() expects integer count or float fraction")
10868                        })?
10869                    }
10870                };
10871                if total_rows == 0 || sample_count == 0 {
10872                    let schema = batches[0].schema();
10873                    let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10874                        schema.clone(),
10875                    );
10876                    let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10877                        .map_err(|e| runtime_err(format!("{e}")))?;
10878                    let new_df = self
10879                        .engine()
10880                        .session_ctx()
10881                        .read_table(Arc::new(mem_table))
10882                        .map_err(|e| runtime_err(format!("{e}")))?;
10883                    return Ok(VmValue::Table(VmTable { df: new_df }));
10884                }
10885                // Random sampling
10886                let mut rng = rand::thread_rng();
10887                let mut indices: Vec<usize> = (0..total_rows).collect();
10888                use rand::seq::SliceRandom;
10889                indices.partial_shuffle(&mut rng, sample_count);
10890                indices.truncate(sample_count);
10891                indices.sort();
10892                // Concatenate and take
10893                let combined = compute::concat_batches(&batches[0].schema(), &batches)
10894                    .map_err(|e| runtime_err(format!("{e}")))?;
10895                let idx_array =
10896                    UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10897                let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10898                    .num_columns())
10899                    .map(|c| {
10900                        compute::take(combined.column(c), &idx_array, None)
10901                            .map_err(|e| runtime_err(format!("{e}")))
10902                    })
10903                    .collect::<Result<Vec<_>, _>>()?;
10904                let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10905                    combined.schema(),
10906                    sampled_cols,
10907                )
10908                .map_err(|e| runtime_err(format!("{e}")))?;
10909                let mem_table =
10910                    MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10911                        .map_err(|e| runtime_err(format!("{e}")))?;
10912                let new_df = self
10913                    .engine()
10914                    .session_ctx()
10915                    .read_table(Arc::new(mem_table))
10916                    .map_err(|e| runtime_err(format!("{e}")))?;
10917                Ok(VmValue::Table(VmTable { df: new_df }))
10918            }
10919            _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10920        }
10921    }
10922
10923    /// Fallback for table pipe when left side is not a table.
10924    /// Converts to a regular function/builtin call with left as first arg.
10925    fn table_pipe_fallback(
10926        &mut self,
10927        left_val: VmValue,
10928        frame_idx: usize,
10929        op_const: u8,
10930        args_const: u8,
10931    ) -> Result<VmValue, TlError> {
10932        let frame = &self.frames[frame_idx];
10933        let op_name = match &frame.prototype.constants[op_const as usize] {
10934            Constant::String(s) => s.to_string(),
10935            _ => return Err(runtime_err("Expected string constant for table op")),
10936        };
10937        let ast_args = match &frame.prototype.constants[args_const as usize] {
10938            Constant::AstExprList(args) => args.clone(),
10939            _ => return Err(runtime_err("Expected AST expr list for table args")),
10940        };
10941
10942        // Try as builtin with left as first arg
10943        if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10944            // Evaluate AST args to VM values
10945            let mut all_args = vec![left_val];
10946            for arg in &ast_args {
10947                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10948            }
10949            let args_base = self.stack.len();
10950            for arg in &all_args {
10951                self.stack.push(arg.clone());
10952            }
10953            let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10954            self.stack.truncate(args_base);
10955            return result;
10956        }
10957
10958        // Try as user-defined function
10959        if let Some(func) = self.globals.get(&op_name).cloned() {
10960            let mut all_args = vec![left_val];
10961            for arg in &ast_args {
10962                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10963            }
10964            return self.call_vm_function(&func, &all_args);
10965        }
10966
10967        Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10968    }
10969
10970    /// Build TranslateContext from VM globals and stack.
10971    #[cfg(feature = "native")]
10972    fn build_translate_context(&self) -> TranslateContext {
10973        let mut ctx = TranslateContext::new();
10974        // Add globals
10975        for (name, val) in &self.globals {
10976            let local = match val {
10977                VmValue::Int(n) => Some(LocalValue::Int(*n)),
10978                VmValue::Float(f) => Some(LocalValue::Float(*f)),
10979                VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10980                VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10981                _ => None,
10982            };
10983            if let Some(l) = local {
10984                ctx.locals.insert(name.clone(), l);
10985            }
10986        }
10987        // Add locals from current frame
10988        if let Some(frame) = self.frames.last() {
10989            for local_idx in 0..frame.prototype.num_locals as usize {
10990                if let Some(val) = self.stack.get(frame.base + local_idx) {
10991                    // We'd need local name info — for now, rely on globals
10992                    let _ = val;
10993                }
10994            }
10995        }
10996        ctx
10997    }
10998
10999    /// Evaluate an AST expression to a VmValue.
11000    /// For simple expressions does direct lookup; for complex ones, compiles and runs.
11001    fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
11002        match expr {
11003            AstExpr::Ident(name) => {
11004                // Look up in globals first
11005                if let Some(val) = self.globals.get(name) {
11006                    return Ok(val.clone());
11007                }
11008                // Then resolve a local of the current frame via the name->register map.
11009                if let Some(val) = self.lookup_frame_local(name) {
11010                    return Ok(val);
11011                }
11012                Err(runtime_err(format!("Undefined variable: `{name}`")))
11013            }
11014            AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
11015            AstExpr::Int(n) => Ok(VmValue::Int(*n)),
11016            AstExpr::Float(f) => Ok(VmValue::Float(*f)),
11017            AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
11018            AstExpr::None => Ok(VmValue::None),
11019            AstExpr::Closure {
11020                params: _, body: _, ..
11021            } => {
11022                use crate::compiler;
11023                let wrapper = tl_ast::Program {
11024                    statements: vec![tl_ast::Stmt {
11025                        kind: tl_ast::StmtKind::Expr(expr.clone()),
11026                        span: tl_errors::Span::new(0, 0),
11027                        doc_comment: None,
11028                    }],
11029                    module_doc: None,
11030                };
11031                let proto = compiler::compile(&wrapper)?;
11032                let mut temp_vm = Vm::new();
11033                // Copy globals + the current frame's locals so the expression can
11034                // reference local variables (e.g. a closure capturing a local table).
11035                temp_vm.globals = self.globals.clone();
11036                for (n, v) in self.current_frame_locals() {
11037                    temp_vm.globals.insert(n, v);
11038                }
11039                let result = temp_vm.execute(&proto)?;
11040                Ok(result)
11041            }
11042            _ => {
11043                // For complex expressions, compile and evaluate
11044                let wrapper = tl_ast::Program {
11045                    statements: vec![tl_ast::Stmt {
11046                        kind: tl_ast::StmtKind::Expr(expr.clone()),
11047                        span: tl_errors::Span::new(0, 0),
11048                        doc_comment: None,
11049                    }],
11050                    module_doc: None,
11051                };
11052                use crate::compiler;
11053                let proto = compiler::compile(&wrapper)?;
11054                let mut temp_vm = Vm::new();
11055                // Copy globals + the current frame's locals so the expression can
11056                // reference local variables (e.g. `join(usage.clone(), ...)`).
11057                temp_vm.globals = self.globals.clone();
11058                for (n, v) in self.current_frame_locals() {
11059                    temp_vm.globals.insert(n, v);
11060                }
11061                temp_vm.execute(&proto)
11062            }
11063        }
11064    }
11065
11066    /// Resolve a variable by name in the current frame's locals via the
11067    /// compiler-emitted name->register map (`top_level_locals`). Returns the most
11068    /// recent binding for the name. Used to evaluate table-op AST args (e.g. the
11069    /// right-hand table of `join`/`union`) that reference local variables.
11070    fn lookup_frame_local(&self, name: &str) -> Option<VmValue> {
11071        let frame = self.frames.last()?;
11072        let mut found = None;
11073        for (local_name, reg) in &frame.prototype.top_level_locals {
11074            if local_name == name {
11075                let idx = frame.base + *reg as usize;
11076                if let Some(v) = self.stack.get(idx)
11077                    && !matches!(v, VmValue::None)
11078                {
11079                    found = Some(v.clone()); // keep the last (most recent) binding
11080                }
11081            }
11082        }
11083        found
11084    }
11085
11086    /// Snapshot the current frame's named locals as (name, value) pairs.
11087    fn current_frame_locals(&self) -> Vec<(String, VmValue)> {
11088        let mut out = Vec::new();
11089        if let Some(frame) = self.frames.last() {
11090            for (name, reg) in &frame.prototype.top_level_locals {
11091                let idx = frame.base + *reg as usize;
11092                if let Some(v) = self.stack.get(idx)
11093                    && !matches!(v, VmValue::None)
11094                {
11095                    out.push((name.clone(), v.clone()));
11096                }
11097            }
11098        }
11099        out
11100    }
11101
11102    fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
11103        match self.eval_ast_to_vm(expr)? {
11104            VmValue::String(s) => Ok(s.to_string()),
11105            _ => Err(runtime_err("Expected a string")),
11106        }
11107    }
11108
11109    /// Simple string interpolation.
11110    fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
11111        let mut result = String::new();
11112        let mut chars = s.chars().peekable();
11113        while let Some(ch) = chars.next() {
11114            if ch == '{' {
11115                let mut var_name = String::new();
11116                let mut depth = 1;
11117                for c in chars.by_ref() {
11118                    if c == '{' {
11119                        depth += 1;
11120                    } else if c == '}' {
11121                        depth -= 1;
11122                        if depth == 0 {
11123                            break;
11124                        }
11125                    }
11126                    var_name.push(c);
11127                }
11128                // Look up variable
11129                if let Some(val) = self.globals.get(&var_name) {
11130                    result.push_str(&format!("{val}"));
11131                } else {
11132                    // Check locals in current frame
11133                    // For now, fall back to globals only — local name info
11134                    // would need debug symbols from the compiler
11135                    result.push('{');
11136                    result.push_str(&var_name);
11137                    result.push('}');
11138                }
11139            } else if ch == '\\' {
11140                match chars.next() {
11141                    Some('n') => result.push('\n'),
11142                    Some('t') => result.push('\t'),
11143                    Some('\\') => result.push('\\'),
11144                    Some('"') => result.push('"'),
11145                    Some(c) => {
11146                        result.push('\\');
11147                        result.push(c);
11148                    }
11149                    None => result.push('\\'),
11150                }
11151            } else {
11152                result.push(ch);
11153            }
11154        }
11155        Ok(result)
11156    }
11157
11158    /// Execute a single bytecode instruction at the given base offset.
11159    /// Used by the LLVM backend's Tier 3 fallback to run complex opcodes on the VM.
11160    pub fn execute_single_instruction(
11161        &mut self,
11162        inst: u32,
11163        proto: &Prototype,
11164        base: usize,
11165    ) -> Result<Option<VmValue>, TlError> {
11166        use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
11167
11168        let proto = Arc::new(proto.clone());
11169        // Push a temporary call frame so the VM can resolve constants etc.
11170        self.frames.push(CallFrame {
11171            prototype: proto.clone(),
11172            ip: 0,
11173            base,
11174            upvalues: Vec::new(),
11175        });
11176        let frame_idx = self.frames.len() - 1;
11177
11178        let op = decode_op(inst);
11179        let a = decode_a(inst);
11180        let _b = decode_b(inst);
11181        let _c = decode_c(inst);
11182        let bx = decode_bx(inst);
11183
11184        // Dispatch the single opcode. We handle the most common
11185        // Tier 3 ops here; anything not handled returns Ok(None).
11186        let result = match op {
11187            Op::GetGlobal => {
11188                let name = self.get_string_constant(frame_idx, bx)?;
11189                let val = self
11190                    .globals
11191                    .get(name.as_ref())
11192                    .cloned()
11193                    .unwrap_or(VmValue::None);
11194                self.stack[base + a as usize] = val;
11195                Ok(None)
11196            }
11197            Op::SetGlobal => {
11198                let name = self.get_string_constant(frame_idx, bx)?;
11199                let val = self.stack[base + a as usize].clone();
11200                self.globals.insert(name.to_string(), val);
11201                Ok(None)
11202            }
11203            _ => {
11204                // For opcodes not explicitly handled, return Ok — the caller
11205                // should have handled Tier 1/2 in LLVM IR.
11206                Ok(None)
11207            }
11208        };
11209
11210        self.frames.pop();
11211        result
11212    }
11213}
11214
11215impl Default for Vm {
11216    fn default() -> Self {
11217        Self::new()
11218    }
11219}
11220
11221#[cfg(test)]
11222mod tests {
11223    use super::*;
11224    use crate::compiler::compile;
11225    use tl_parser::parse;
11226
11227    fn run(source: &str) -> Result<VmValue, TlError> {
11228        let program = parse(source)?;
11229        let proto = compile(&program)?;
11230        let mut vm = Vm::new();
11231        vm.execute(&proto)
11232    }
11233
11234    fn run_output(source: &str) -> Vec<String> {
11235        let program = parse(source).unwrap();
11236        let proto = compile(&program).unwrap();
11237        let mut vm = Vm::new();
11238        vm.execute(&proto).unwrap();
11239        vm.output
11240    }
11241
11242    #[test]
11243    fn test_vm_arithmetic() {
11244        assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
11245        assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
11246        assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
11247        assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
11248        assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
11249        assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
11250        let output = run_output("print(1 + 2)");
11251        assert_eq!(output, vec!["3"]);
11252    }
11253
11254    #[test]
11255    fn test_vm_let_and_print() {
11256        let output = run_output("let x = 42\nprint(x)");
11257        assert_eq!(output, vec!["42"]);
11258    }
11259
11260    #[test]
11261    fn test_vm_function() {
11262        let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
11263        assert_eq!(output, vec!["42"]);
11264    }
11265
11266    #[test]
11267    fn test_vm_if_else() {
11268        let output =
11269            run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
11270        assert_eq!(output, vec!["big"]);
11271    }
11272
11273    #[test]
11274    fn test_vm_list() {
11275        let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
11276        assert_eq!(output, vec!["3"]);
11277    }
11278
11279    #[test]
11280    fn test_vm_map_builtin() {
11281        let output = run_output(
11282            "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
11283        );
11284        assert_eq!(output, vec!["[2, 4, 6]"]);
11285    }
11286
11287    #[test]
11288    fn test_vm_filter_builtin() {
11289        let output = run_output(
11290            "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
11291        );
11292        assert_eq!(output, vec!["[2, 4]"]);
11293    }
11294
11295    #[test]
11296    fn test_vm_for_loop() {
11297        let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
11298        assert_eq!(output, vec!["10"]);
11299    }
11300
11301    #[test]
11302    fn test_vm_closure() {
11303        let output = run_output("let double = (x) => x * 2\nprint(double(5))");
11304        assert_eq!(output, vec!["10"]);
11305    }
11306
11307    #[test]
11308    fn test_vm_sum() {
11309        let output = run_output("print(sum([1, 2, 3, 4]))");
11310        assert_eq!(output, vec!["10"]);
11311    }
11312
11313    #[test]
11314    fn test_vm_reduce() {
11315        let output = run_output(
11316            "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
11317        );
11318        assert_eq!(output, vec!["24"]);
11319    }
11320
11321    #[test]
11322    fn test_vm_pipe() {
11323        let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
11324        assert_eq!(output, vec!["[11, 12, 13]"]);
11325    }
11326
11327    #[test]
11328    fn test_vm_comparison() {
11329        let output = run_output("print(5 > 3)");
11330        assert_eq!(output, vec!["true"]);
11331    }
11332
11333    #[test]
11334    fn test_vm_precedence() {
11335        let output = run_output("print(2 + 3 * 4)");
11336        assert_eq!(output, vec!["14"]);
11337    }
11338
11339    #[test]
11340    fn test_vm_match() {
11341        let output =
11342            run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
11343        assert_eq!(output, vec!["two"]);
11344    }
11345
11346    #[test]
11347    fn test_vm_match_wildcard() {
11348        let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
11349        assert_eq!(output, vec!["other"]);
11350    }
11351
11352    #[test]
11353    fn test_vm_match_binding() {
11354        let output = run_output("print(match 42 { val => val + 1 })");
11355        assert_eq!(output, vec!["43"]);
11356    }
11357
11358    #[test]
11359    fn test_vm_match_guard() {
11360        let output = run_output(
11361            "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11362        );
11363        assert_eq!(output, vec!["pos"]);
11364    }
11365
11366    #[test]
11367    fn test_vm_match_guard_negative() {
11368        let output = run_output(
11369            "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11370        );
11371        assert_eq!(output, vec!["neg"]);
11372    }
11373
11374    #[test]
11375    fn test_vm_match_guard_zero() {
11376        let output = run_output(
11377            "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11378        );
11379        assert_eq!(output, vec!["zero"]);
11380    }
11381
11382    #[test]
11383    fn test_vm_match_enum_destructure() {
11384        let output = run_output(
11385            r#"
11386enum Shape { Circle(int64), Rect(int64, int64) }
11387let s = Shape::Circle(5)
11388print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11389"#,
11390        );
11391        assert_eq!(output, vec!["5"]);
11392    }
11393
11394    #[test]
11395    fn test_vm_match_enum_destructure_rect() {
11396        let output = run_output(
11397            r#"
11398enum Shape { Circle(int64), Rect(int64, int64) }
11399let s = Shape::Rect(3, 4)
11400print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11401"#,
11402        );
11403        assert_eq!(output, vec!["12"]);
11404    }
11405
11406    #[test]
11407    fn test_vm_match_enum_wildcard_field() {
11408        let output = run_output(
11409            r#"
11410enum Pair { Two(int64, int64) }
11411let p = Pair::Two(10, 20)
11412print(match p { Pair::Two(_, y) => y, _ => 0 })
11413"#,
11414        );
11415        assert_eq!(output, vec!["20"]);
11416    }
11417
11418    #[test]
11419    fn test_vm_match_enum_guard() {
11420        let output = run_output(
11421            r#"
11422enum Result { Ok(int64), Err(string) }
11423let r = Result::Ok(150)
11424print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
11425"#,
11426        );
11427        assert_eq!(output, vec!["big"]);
11428    }
11429
11430    #[test]
11431    fn test_vm_match_or_pattern() {
11432        let output =
11433            run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11434        assert_eq!(output, vec!["small"]);
11435    }
11436
11437    #[test]
11438    fn test_vm_match_or_pattern_no_match() {
11439        let output =
11440            run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11441        assert_eq!(output, vec!["big"]);
11442    }
11443
11444    #[test]
11445    fn test_vm_match_string() {
11446        let output = run_output(
11447            r#"let s = "hello"
11448print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
11449        );
11450        assert_eq!(output, vec!["2"]);
11451    }
11452
11453    #[test]
11454    fn test_vm_match_bool() {
11455        let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
11456        assert_eq!(output, vec!["yes"]);
11457    }
11458
11459    #[test]
11460    fn test_vm_match_none() {
11461        let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
11462        assert_eq!(output, vec!["nothing"]);
11463    }
11464
11465    #[test]
11466    fn test_vm_let_destructure_list() {
11467        let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
11468        assert_eq!(output, vec!["1", "2", "3"]);
11469    }
11470
11471    #[test]
11472    fn test_vm_let_destructure_list_rest() {
11473        let output =
11474            run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
11475        assert_eq!(output, vec!["1", "3"]);
11476    }
11477
11478    #[test]
11479    fn test_vm_let_destructure_struct() {
11480        let output = run_output(
11481            r#"
11482struct Point { x: int64, y: int64 }
11483let p = Point { x: 10, y: 20 }
11484let Point { x, y } = p
11485print(x)
11486print(y)
11487"#,
11488        );
11489        assert_eq!(output, vec!["10", "20"]);
11490    }
11491
11492    #[test]
11493    fn test_vm_let_destructure_struct_anon() {
11494        let output = run_output(
11495            r#"
11496struct Point { x: int64, y: int64 }
11497let p = Point { x: 10, y: 20 }
11498let { x, y } = p
11499print(x)
11500print(y)
11501"#,
11502        );
11503        assert_eq!(output, vec!["10", "20"]);
11504    }
11505
11506    #[test]
11507    fn test_vm_match_struct_pattern() {
11508        let output = run_output(
11509            r#"
11510struct Point { x: int64, y: int64 }
11511let p = Point { x: 1, y: 2 }
11512print(match p { Point { x, y } => x + y, _ => 0 })
11513"#,
11514        );
11515        assert_eq!(output, vec!["3"]);
11516    }
11517
11518    #[test]
11519    fn test_vm_match_list_pattern() {
11520        let output = run_output(
11521            r#"
11522let lst = [1, 2, 3]
11523print(match lst { [a, b, c] => a + b + c, _ => 0 })
11524"#,
11525        );
11526        assert_eq!(output, vec!["6"]);
11527    }
11528
11529    #[test]
11530    fn test_vm_match_list_rest_pattern() {
11531        let output = run_output(
11532            r#"
11533let lst = [10, 20, 30, 40]
11534print(match lst { [head, ...rest] => head, _ => 0 })
11535"#,
11536        );
11537        assert_eq!(output, vec!["10"]);
11538    }
11539
11540    #[test]
11541    fn test_vm_match_list_empty() {
11542        let output = run_output(
11543            r#"
11544let lst = []
11545print(match lst { [] => "empty", _ => "nonempty" })
11546"#,
11547        );
11548        assert_eq!(output, vec!["empty"]);
11549    }
11550
11551    #[test]
11552    fn test_vm_match_list_length_mismatch() {
11553        let output = run_output(
11554            r#"
11555let lst = [1, 2, 3]
11556print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
11557"#,
11558        );
11559        assert_eq!(output, vec!["three"]);
11560    }
11561
11562    #[test]
11563    fn test_vm_match_negative_literal() {
11564        let output =
11565            run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
11566        assert_eq!(output, vec!["neg one"]);
11567    }
11568
11569    #[test]
11570    fn test_vm_case_with_pattern() {
11571        let output = run_output(
11572            r#"
11573let x = 5
11574let result = case {
11575    x > 10 => "big",
11576    x > 0 => "positive",
11577    _ => "other"
11578}
11579print(result)
11580"#,
11581        );
11582        assert_eq!(output, vec!["positive"]);
11583    }
11584
11585    #[test]
11586    fn test_vm_parallel_map() {
11587        // Build a range > PARALLEL_THRESHOLD and map with a pure function
11588        let result = run("map(range(15000), (x) => x * 2)").unwrap();
11589        if let VmValue::List(items) = result {
11590            assert_eq!(items.len(), 15000);
11591            assert!(matches!(items[0], VmValue::Int(0)));
11592            assert!(matches!(items[1], VmValue::Int(2)));
11593            assert!(matches!(items[14999], VmValue::Int(29998)));
11594        } else {
11595            panic!("Expected list, got {:?}", result);
11596        }
11597    }
11598
11599    #[test]
11600    fn test_vm_parallel_filter() {
11601        let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11602        if let VmValue::List(items) = result {
11603            assert_eq!(items.len(), 10000);
11604            assert!(matches!(items[0], VmValue::Int(0)));
11605            assert!(matches!(items[1], VmValue::Int(2)));
11606        } else {
11607            panic!("Expected list, got {:?}", result);
11608        }
11609    }
11610
11611    #[test]
11612    fn test_vm_parallel_sum() {
11613        let result = run("sum(range(20000))").unwrap();
11614        // sum(0..19999) = 19999 * 20000 / 2 = 199990000
11615        assert!(matches!(result, VmValue::Int(199990000)));
11616    }
11617
11618    #[test]
11619    fn test_vm_recursive_fib() {
11620        let output = run_output(
11621            "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11622        );
11623        assert_eq!(output, vec!["55"]);
11624    }
11625
11626    #[test]
11627    fn test_vm_if_else_expr() {
11628        // if-else as the last expression in a function should return a value
11629        let output = run_output(
11630            "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11631        );
11632        assert_eq!(output, vec!["5", "3"]);
11633    }
11634
11635    // ── Phase 5 tests ──
11636
11637    #[test]
11638    fn test_vm_struct_creation() {
11639        let output = run_output(
11640            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11641        );
11642        assert_eq!(output, vec!["1.0", "2.0"]);
11643    }
11644
11645    #[test]
11646    fn test_vm_struct_nested() {
11647        let output = run_output(
11648            "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)",
11649        );
11650        assert_eq!(output, vec!["0.0"]);
11651    }
11652
11653    #[test]
11654    fn test_vm_enum_creation() {
11655        let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11656        assert_eq!(output, vec!["Color::Red"]);
11657    }
11658
11659    #[test]
11660    fn test_vm_enum_with_fields() {
11661        let output = run_output(
11662            "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11663        );
11664        assert!(output[0].contains("Circle"));
11665    }
11666
11667    #[test]
11668    fn test_vm_impl_method() {
11669        let output = run_output(
11670            "struct Counter { value: int64 }\nimpl Counter {\n    fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11671        );
11672        assert_eq!(output, vec!["42"]);
11673    }
11674
11675    #[test]
11676    fn test_vm_try_catch_throw() {
11677        let output = run_output("try {\n    throw \"oops\"\n} catch e {\n    print(e)\n}");
11678        assert_eq!(output, vec!["oops"]);
11679    }
11680
11681    #[test]
11682    fn test_vm_string_split() {
11683        let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11684        assert_eq!(output, vec!["[hello, world]"]);
11685    }
11686
11687    #[test]
11688    fn test_vm_string_trim() {
11689        let output = run_output("print(\"  hello  \".trim())");
11690        assert_eq!(output, vec!["hello"]);
11691    }
11692
11693    #[test]
11694    fn test_vm_string_contains() {
11695        let output = run_output("print(\"hello world\".contains(\"world\"))");
11696        assert_eq!(output, vec!["true"]);
11697    }
11698
11699    #[test]
11700    fn test_vm_string_upper_lower() {
11701        let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11702        assert_eq!(output, vec!["HELLO", "hello"]);
11703    }
11704
11705    #[test]
11706    fn test_vm_math_sqrt() {
11707        let output = run_output("print(sqrt(16.0))");
11708        assert_eq!(output, vec!["4.0"]);
11709    }
11710
11711    #[test]
11712    fn test_vm_math_floor_ceil() {
11713        let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11714        assert_eq!(output, vec!["3.0", "4.0"]);
11715    }
11716
11717    #[test]
11718    fn test_vm_math_trig() {
11719        let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11720        assert_eq!(output, vec!["0.0", "1.0"]);
11721    }
11722
11723    #[test]
11724    fn test_vm_assert_pass() {
11725        run("assert(true)").unwrap();
11726        run("assert_eq(1 + 1, 2)").unwrap();
11727    }
11728
11729    #[test]
11730    fn test_vm_assert_fail() {
11731        assert!(run("assert(false)").is_err());
11732        assert!(run("assert_eq(1, 2)").is_err());
11733    }
11734
11735    #[test]
11736    fn test_vm_join() {
11737        let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11738        assert_eq!(output, vec!["a, b, c"]);
11739    }
11740
11741    #[test]
11742    fn test_vm_list_method_len() {
11743        let output = run_output("print([1, 2, 3].len())");
11744        assert_eq!(output, vec!["3"]);
11745    }
11746
11747    #[test]
11748    fn test_vm_list_method_map() {
11749        let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11750        assert_eq!(output, vec!["[2, 4, 6]"]);
11751    }
11752
11753    #[test]
11754    fn test_vm_list_method_filter() {
11755        let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11756        assert_eq!(output, vec!["[4, 5]"]);
11757    }
11758
11759    #[test]
11760    fn test_vm_string_replace() {
11761        let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11762        assert_eq!(output, vec!["hello rust"]);
11763    }
11764
11765    #[test]
11766    fn test_vm_string_starts_ends() {
11767        let output = run_output(
11768            "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11769        );
11770        assert_eq!(output, vec!["true", "true"]);
11771    }
11772
11773    #[test]
11774    fn test_vm_math_log() {
11775        let result = run("log(1.0)").unwrap();
11776        if let VmValue::Float(f) = result {
11777            assert!((f - 0.0).abs() < 1e-10);
11778        } else {
11779            panic!("Expected float");
11780        }
11781    }
11782
11783    #[test]
11784    fn test_vm_pow_builtin() {
11785        let output = run_output("print(pow(2.0, 10.0))");
11786        assert_eq!(output, vec!["1024.0"]);
11787    }
11788
11789    #[test]
11790    fn test_vm_round_builtin() {
11791        let output = run_output("print(round(3.5))");
11792        assert_eq!(output, vec!["4.0"]);
11793    }
11794
11795    #[test]
11796    fn test_vm_try_catch_runtime_error() {
11797        let output = run_output("try {\n    let x = 1 / 0\n} catch e {\n    print(e)\n}");
11798        assert_eq!(output, vec!["Division by zero"]);
11799    }
11800
11801    #[test]
11802    fn test_vm_struct_field_access() {
11803        let output = run_output(
11804            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11805        );
11806        assert_eq!(output, vec!["1.5"]);
11807    }
11808
11809    #[test]
11810    fn test_vm_enum_match() {
11811        let output = run_output(
11812            "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11813        );
11814        // match expression compares enum instances
11815        assert!(!output.is_empty());
11816    }
11817
11818    #[test]
11819    fn test_vm_impl_method_with_args() {
11820        let output = run_output(
11821            "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())",
11822        );
11823        assert_eq!(output, vec!["12.0"]);
11824    }
11825
11826    #[test]
11827    fn test_vm_string_len() {
11828        let output = run_output("print(\"hello\".len())");
11829        assert_eq!(output, vec!["5"]);
11830    }
11831
11832    #[test]
11833    fn test_vm_list_reduce() {
11834        let output = run_output(
11835            "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11836        );
11837        assert_eq!(output, vec!["10"]);
11838    }
11839
11840    #[test]
11841    fn test_vm_nested_try_catch() {
11842        let output = run_output(
11843            "try {\n    try {\n        throw \"inner\"\n    } catch e {\n        print(e)\n        throw \"outer\"\n    }\n} catch e2 {\n    print(e2)\n}",
11844        );
11845        assert_eq!(output, vec!["inner", "outer"]);
11846    }
11847
11848    #[test]
11849    fn test_vm_math_pow() {
11850        let output = run_output("print(pow(2.0, 10.0))");
11851        assert_eq!(output, vec!["1024.0"]);
11852    }
11853
11854    // ── Phase 6: Stdlib & Ecosystem tests ──
11855
11856    #[test]
11857    fn test_vm_json_parse() {
11858        let output = run_output(
11859            r#"let m = map_from("a", 1, "b", "hello")
11860let s = json_stringify(m)
11861let m2 = json_parse(s)
11862print(m2["a"])
11863print(m2["b"])"#,
11864        );
11865        assert_eq!(output, vec!["1", "hello"]);
11866    }
11867
11868    #[test]
11869    fn test_vm_json_stringify() {
11870        let output = run_output(
11871            r#"let m = map_from("x", 1, "y", 2)
11872let s = json_stringify(m)
11873print(s)"#,
11874        );
11875        assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11876    }
11877
11878    #[test]
11879    fn test_vm_map_from_and_access() {
11880        let output = run_output(
11881            r#"let m = map_from("a", 10, "b", 20)
11882print(m["a"])
11883print(m.b)"#,
11884        );
11885        assert_eq!(output, vec!["10", "20"]);
11886    }
11887
11888    #[test]
11889    fn test_vm_map_methods() {
11890        let output = run_output(
11891            r#"let m = map_from("a", 1, "b", 2)
11892print(m.keys())
11893print(m.values())
11894print(m.contains_key("a"))
11895print(m.len())"#,
11896        );
11897        assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11898    }
11899
11900    #[test]
11901    fn test_vm_map_set_index() {
11902        let output = run_output(
11903            r#"let m = map_from("a", 1)
11904m["b"] = 2
11905print(m["b"])"#,
11906        );
11907        assert_eq!(output, vec!["2"]);
11908    }
11909
11910    #[test]
11911    fn test_vm_map_iteration() {
11912        let output = run_output(
11913            r#"let m = map_from("x", 10, "y", 20)
11914for kv in m {
11915    print(kv[0])
11916}"#,
11917        );
11918        assert_eq!(output, vec!["x", "y"]);
11919    }
11920
11921    #[test]
11922    fn test_vm_file_read_write() {
11923        let output = run_output(
11924            r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11925print(read_file("/tmp/tl_vm_test.txt"))
11926print(file_exists("/tmp/tl_vm_test.txt"))"#,
11927        );
11928        assert_eq!(output, vec!["vm hello", "true"]);
11929    }
11930
11931    #[test]
11932    fn test_vm_env_get_set() {
11933        let output = run_output(
11934            r#"env_set("TL_VM_TEST", "abc")
11935print(env_get("TL_VM_TEST"))"#,
11936        );
11937        assert_eq!(output, vec!["abc"]);
11938    }
11939
11940    #[test]
11941    fn test_vm_regex_match() {
11942        let output = run_output(
11943            r#"print(regex_match("\\d+", "abc123"))
11944print(regex_match("^\\d+$", "abc"))"#,
11945        );
11946        assert_eq!(output, vec!["true", "false"]);
11947    }
11948
11949    #[test]
11950    fn test_vm_regex_find() {
11951        let output = run_output(
11952            r#"let m = regex_find("\\d+", "abc123def456")
11953print(len(m))
11954print(m[0])"#,
11955        );
11956        assert_eq!(output, vec!["2", "123"]);
11957    }
11958
11959    #[test]
11960    fn test_vm_regex_replace() {
11961        let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11962        assert_eq!(output, vec!["abcX"]);
11963    }
11964
11965    #[test]
11966    fn test_vm_now() {
11967        // now() returns DateTime which displays as formatted string
11968        let output = run_output("let t = now()\nprint(type_of(t))");
11969        assert_eq!(output, vec!["datetime"]);
11970    }
11971
11972    #[test]
11973    fn test_vm_date_format() {
11974        let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11975        assert_eq!(output, vec!["2024-01-01"]);
11976    }
11977
11978    #[test]
11979    fn test_vm_date_parse() {
11980        let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11981        assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11982    }
11983
11984    #[test]
11985    fn test_vm_string_chars() {
11986        let output = run_output(r#"print(len("hello".chars()))"#);
11987        assert_eq!(output, vec!["5"]);
11988    }
11989
11990    #[test]
11991    fn test_vm_string_repeat() {
11992        let output = run_output(r#"print("ab".repeat(3))"#);
11993        assert_eq!(output, vec!["ababab"]);
11994    }
11995
11996    #[test]
11997    fn test_vm_string_index_of() {
11998        let output = run_output(r#"print("hello world".index_of("world"))"#);
11999        assert_eq!(output, vec!["6"]);
12000    }
12001
12002    #[test]
12003    fn test_vm_string_substring() {
12004        let output = run_output(r#"print("hello world".substring(0, 5))"#);
12005        assert_eq!(output, vec!["hello"]);
12006    }
12007
12008    #[test]
12009    fn test_vm_string_pad() {
12010        let output = run_output(
12011            r#"print("42".pad_left(5, "0"))
12012print("hi".pad_right(5, "."))"#,
12013        );
12014        assert_eq!(output, vec!["00042", "hi..."]);
12015    }
12016
12017    #[test]
12018    fn test_vm_list_sort() {
12019        let output = run_output(r#"print([3, 1, 2].sort())"#);
12020        assert_eq!(output, vec!["[1, 2, 3]"]);
12021    }
12022
12023    #[test]
12024    fn test_vm_list_reverse() {
12025        let output = run_output(r#"print([1, 2, 3].reverse())"#);
12026        assert_eq!(output, vec!["[3, 2, 1]"]);
12027    }
12028
12029    #[test]
12030    fn test_vm_list_contains() {
12031        let output = run_output(
12032            r#"print([1, 2, 3].contains(2))
12033print([1, 2, 3].contains(5))"#,
12034        );
12035        assert_eq!(output, vec!["true", "false"]);
12036    }
12037
12038    #[test]
12039    fn test_vm_list_slice() {
12040        let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
12041        assert_eq!(output, vec!["[2, 3, 4]"]);
12042    }
12043
12044    #[test]
12045    fn test_vm_zip() {
12046        let output = run_output(
12047            r#"let p = zip([1, 2], ["a", "b"])
12048print(p[0])"#,
12049        );
12050        assert_eq!(output, vec!["[1, a]"]);
12051    }
12052
12053    #[test]
12054    fn test_vm_enumerate() {
12055        let output = run_output(
12056            r#"let e = enumerate(["a", "b", "c"])
12057print(e[1])"#,
12058        );
12059        assert_eq!(output, vec!["[1, b]"]);
12060    }
12061
12062    #[test]
12063    fn test_vm_bool() {
12064        let output = run_output(
12065            r#"print(bool(1))
12066print(bool(0))
12067print(bool(""))"#,
12068        );
12069        assert_eq!(output, vec!["true", "false", "false"]);
12070    }
12071
12072    #[test]
12073    fn test_vm_range_step() {
12074        let output = run_output(r#"print(range(0, 10, 3))"#);
12075        assert_eq!(output, vec!["[0, 3, 6, 9]"]);
12076    }
12077
12078    #[test]
12079    fn test_vm_int_bool() {
12080        let output = run_output(
12081            r#"print(int(true))
12082print(int(false))"#,
12083        );
12084        assert_eq!(output, vec!["1", "0"]);
12085    }
12086
12087    #[test]
12088    fn test_vm_map_len_typeof() {
12089        let output = run_output(
12090            r#"let m = map_from("a", 1)
12091print(len(m))
12092print(type_of(m))"#,
12093        );
12094        assert_eq!(output, vec!["1", "map"]);
12095    }
12096
12097    #[test]
12098    fn test_vm_json_file_roundtrip() {
12099        let output = run_output(
12100            r#"let data = map_from("name", "vm_test", "count", 99)
12101write_file("/tmp/tl_vm_json.json", json_stringify(data))
12102let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
12103print(parsed["name"])
12104print(parsed["count"])"#,
12105        );
12106        assert_eq!(output, vec!["vm_test", "99"]);
12107    }
12108
12109    // ── Phase 7: Concurrency tests ──
12110
12111    #[test]
12112    fn test_vm_spawn_await_basic() {
12113        let output = run_output(
12114            r#"fn worker() { 42 }
12115let t = spawn(worker)
12116let result = await t
12117print(result)"#,
12118        );
12119        assert_eq!(output, vec!["42"]);
12120    }
12121
12122    #[test]
12123    fn test_vm_spawn_closure_with_capture() {
12124        let output = run_output(
12125            r#"let x = 10
12126let f = () => x + 5
12127let t = spawn(f)
12128print(await t)"#,
12129        );
12130        assert_eq!(output, vec!["15"]);
12131    }
12132
12133    #[test]
12134    fn test_vm_sleep() {
12135        let output = run_output(
12136            r#"sleep(10)
12137print("done")"#,
12138        );
12139        assert_eq!(output, vec!["done"]);
12140    }
12141
12142    #[test]
12143    fn test_vm_await_non_task_passthrough() {
12144        let output = run_output(r#"print(await 42)"#);
12145        assert_eq!(output, vec!["42"]);
12146    }
12147
12148    #[test]
12149    fn test_vm_spawn_multiple_await() {
12150        let output = run_output(
12151            r#"fn w1() { 1 }
12152fn w2() { 2 }
12153fn w3() { 3 }
12154let t1 = spawn(w1)
12155let t2 = spawn(w2)
12156let t3 = spawn(w3)
12157let a = await t1
12158let b = await t2
12159let c = await t3
12160print(a + b + c)"#,
12161        );
12162        assert_eq!(output, vec!["6"]);
12163    }
12164
12165    #[test]
12166    fn test_vm_channel_basic() {
12167        let output = run_output(
12168            r#"let ch = channel()
12169send(ch, 42)
12170let val = recv(ch)
12171print(val)"#,
12172        );
12173        assert_eq!(output, vec!["42"]);
12174    }
12175
12176    #[test]
12177    fn test_vm_channel_between_tasks() {
12178        let output = run_output(
12179            r#"let ch = channel()
12180fn producer() { send(ch, 100) }
12181let t = spawn(producer)
12182let val = recv(ch)
12183await t
12184print(val)"#,
12185        );
12186        assert_eq!(output, vec!["100"]);
12187    }
12188
12189    #[test]
12190    fn test_vm_try_recv_empty() {
12191        let output = run_output(
12192            r#"let ch = channel()
12193let val = try_recv(ch)
12194print(val)"#,
12195        );
12196        assert_eq!(output, vec!["none"]);
12197    }
12198
12199    #[test]
12200    fn test_vm_channel_multiple_values() {
12201        let output = run_output(
12202            r#"let ch = channel()
12203send(ch, 1)
12204send(ch, 2)
12205send(ch, 3)
12206print(recv(ch))
12207print(recv(ch))
12208print(recv(ch))"#,
12209        );
12210        assert_eq!(output, vec!["1", "2", "3"]);
12211    }
12212
12213    #[test]
12214    fn test_vm_channel_producer_consumer() {
12215        let output = run_output(
12216            r#"let ch = channel()
12217fn producer() {
12218    send(ch, 10)
12219    send(ch, 20)
12220    send(ch, 30)
12221}
12222let t = spawn(producer)
12223let a = recv(ch)
12224let b = recv(ch)
12225let c = recv(ch)
12226await t
12227print(a + b + c)"#,
12228        );
12229        assert_eq!(output, vec!["60"]);
12230    }
12231
12232    #[test]
12233    fn test_vm_await_all() {
12234        let output = run_output(
12235            r#"fn w1() { 10 }
12236fn w2() { 20 }
12237fn w3() { 30 }
12238let t1 = spawn(w1)
12239let t2 = spawn(w2)
12240let t3 = spawn(w3)
12241let results = await_all([t1, t2, t3])
12242print(sum(results))"#,
12243        );
12244        assert_eq!(output, vec!["60"]);
12245    }
12246
12247    #[test]
12248    fn test_vm_pmap_basic() {
12249        let output = run_output(
12250            r#"let results = pmap([1, 2, 3], (x) => x * 2)
12251print(results)"#,
12252        );
12253        assert_eq!(output, vec!["[2, 4, 6]"]);
12254    }
12255
12256    #[test]
12257    fn test_vm_pmap_order_preserved() {
12258        let output = run_output(
12259            r#"let results = pmap([10, 20, 30], (x) => x + 1)
12260print(results)"#,
12261        );
12262        assert_eq!(output, vec!["[11, 21, 31]"]);
12263    }
12264
12265    #[test]
12266    fn test_vm_timeout_success() {
12267        let output = run_output(
12268            r#"fn worker() { 42 }
12269let t = spawn(worker)
12270let result = timeout(t, 5000)
12271print(result)"#,
12272        );
12273        assert_eq!(output, vec!["42"]);
12274    }
12275
12276    #[test]
12277    fn test_vm_timeout_failure() {
12278        let output = run_output(
12279            r#"fn slow() { sleep(10000) }
12280let t = spawn(slow)
12281let result = "ok"
12282try {
12283    result = timeout(t, 50)
12284} catch e {
12285    result = e
12286}
12287print(result)"#,
12288        );
12289        assert_eq!(output, vec!["Task timed out"]);
12290    }
12291
12292    #[test]
12293    fn test_vm_spawn_error_propagation() {
12294        let output = run_output(
12295            r#"fn bad() { throw "bad thing" }
12296let result = "ok"
12297try {
12298    let t = spawn(bad)
12299    result = await t
12300} catch e {
12301    result = e
12302}
12303print(result)"#,
12304        );
12305        assert_eq!(output, vec!["bad thing"]);
12306    }
12307
12308    #[test]
12309    fn test_vm_spawn_producer_consumer_pipeline() {
12310        let output = run_output(
12311            r#"let ch = channel()
12312fn producer() {
12313    let mut i = 0
12314    while i < 5 {
12315        send(ch, i * 10)
12316        i = i + 1
12317    }
12318}
12319let t = spawn(producer)
12320let mut total = 0
12321let mut count = 0
12322while count < 5 {
12323    total = total + recv(ch)
12324    count = count + 1
12325}
12326await t
12327print(total)"#,
12328        );
12329        assert_eq!(output, vec!["100"]);
12330    }
12331
12332    #[test]
12333    fn test_vm_type_of_task_channel() {
12334        let output = run_output(
12335            r#"fn worker() { 1 }
12336let t = spawn(worker)
12337let ch = channel()
12338print(type_of(t))
12339print(type_of(ch))
12340await t"#,
12341        );
12342        assert_eq!(output, vec!["task", "channel"]);
12343    }
12344
12345    // ── Phase 8: Iterators & Generators ──
12346
12347    #[test]
12348    fn test_vm_basic_generator() {
12349        let output = run_output(
12350            r#"fn gen() {
12351    yield 1
12352    yield 2
12353    yield 3
12354}
12355let g = gen()
12356print(next(g))
12357print(next(g))
12358print(next(g))
12359print(next(g))"#,
12360        );
12361        assert_eq!(output, vec!["1", "2", "3", "none"]);
12362    }
12363
12364    #[test]
12365    fn test_vm_generator_exhaustion() {
12366        let output = run_output(
12367            r#"fn gen() {
12368    yield 42
12369}
12370let g = gen()
12371print(next(g))
12372print(next(g))
12373print(next(g))"#,
12374        );
12375        assert_eq!(output, vec!["42", "none", "none"]);
12376    }
12377
12378    #[test]
12379    fn test_vm_generator_with_loop() {
12380        let output = run_output(
12381            r#"fn counter() {
12382    let mut i = 0
12383    while i < 3 {
12384        yield i
12385        i = i + 1
12386    }
12387}
12388let g = counter()
12389print(next(g))
12390print(next(g))
12391print(next(g))
12392print(next(g))"#,
12393        );
12394        assert_eq!(output, vec!["0", "1", "2", "none"]);
12395    }
12396
12397    #[test]
12398    fn test_vm_generator_with_args() {
12399        let output = run_output(
12400            r#"fn count_from(start) {
12401    let mut i = start
12402    while i < start + 3 {
12403        yield i
12404        i = i + 1
12405    }
12406}
12407let g = count_from(10)
12408print(next(g))
12409print(next(g))
12410print(next(g))
12411print(next(g))"#,
12412        );
12413        assert_eq!(output, vec!["10", "11", "12", "none"]);
12414    }
12415
12416    #[test]
12417    fn test_vm_generator_yield_none() {
12418        let output = run_output(
12419            r#"fn gen() {
12420    yield
12421    yield 5
12422}
12423let g = gen()
12424print(next(g))
12425print(next(g))
12426print(next(g))"#,
12427        );
12428        assert_eq!(output, vec!["none", "5", "none"]);
12429    }
12430
12431    #[test]
12432    fn test_vm_is_generator() {
12433        let output = run_output(
12434            r#"fn gen() { yield 1 }
12435let g = gen()
12436print(is_generator(g))
12437print(is_generator(42))
12438print(is_generator(none))"#,
12439        );
12440        assert_eq!(output, vec!["true", "false", "false"]);
12441    }
12442
12443    #[test]
12444    fn test_vm_multiple_generators() {
12445        let output = run_output(
12446            r#"fn gen() {
12447    yield 1
12448    yield 2
12449}
12450let g1 = gen()
12451let g2 = gen()
12452print(next(g1))
12453print(next(g2))
12454print(next(g1))
12455print(next(g2))"#,
12456        );
12457        assert_eq!(output, vec!["1", "1", "2", "2"]);
12458    }
12459
12460    #[test]
12461    fn test_vm_for_over_generator() {
12462        let output = run_output(
12463            r#"fn gen() {
12464    yield 10
12465    yield 20
12466    yield 30
12467}
12468for x in gen() {
12469    print(x)
12470}"#,
12471        );
12472        assert_eq!(output, vec!["10", "20", "30"]);
12473    }
12474
12475    #[test]
12476    fn test_vm_iter_builtin() {
12477        let output = run_output(
12478            r#"let g = iter([1, 2, 3])
12479print(next(g))
12480print(next(g))
12481print(next(g))
12482print(next(g))"#,
12483        );
12484        assert_eq!(output, vec!["1", "2", "3", "none"]);
12485    }
12486
12487    #[test]
12488    fn test_vm_take_builtin() {
12489        let output = run_output(
12490            r#"fn naturals() {
12491    let mut n = 0
12492    while true {
12493        yield n
12494        n = n + 1
12495    }
12496}
12497let g = take(naturals(), 5)
12498print(next(g))
12499print(next(g))
12500print(next(g))
12501print(next(g))
12502print(next(g))
12503print(next(g))"#,
12504        );
12505        assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
12506    }
12507
12508    #[test]
12509    fn test_vm_skip_builtin() {
12510        let output = run_output(
12511            r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
12512print(next(g))
12513print(next(g))
12514print(next(g))
12515print(next(g))"#,
12516        );
12517        assert_eq!(output, vec!["30", "40", "50", "none"]);
12518    }
12519
12520    #[test]
12521    fn test_vm_gen_collect() {
12522        let output = run_output(
12523            r#"fn gen() {
12524    yield 1
12525    yield 2
12526    yield 3
12527}
12528let result = gen_collect(gen())
12529print(result)"#,
12530        );
12531        assert_eq!(output, vec!["[1, 2, 3]"]);
12532    }
12533
12534    #[test]
12535    fn test_vm_gen_map() {
12536        let output = run_output(
12537            r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
12538print(gen_collect(g))"#,
12539        );
12540        assert_eq!(output, vec!["[10, 20, 30]"]);
12541    }
12542
12543    #[test]
12544    fn test_vm_gen_filter() {
12545        let output = run_output(
12546            r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
12547print(gen_collect(g))"#,
12548        );
12549        assert_eq!(output, vec!["[2, 4, 6]"]);
12550    }
12551
12552    #[test]
12553    fn test_vm_chain() {
12554        let output = run_output(
12555            r#"let g = chain(iter([1, 2]), iter([3, 4]))
12556print(gen_collect(g))"#,
12557        );
12558        assert_eq!(output, vec!["[1, 2, 3, 4]"]);
12559    }
12560
12561    #[test]
12562    fn test_vm_gen_zip() {
12563        let output = run_output(
12564            r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
12565print(gen_collect(g))"#,
12566        );
12567        assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
12568    }
12569
12570    #[test]
12571    fn test_vm_gen_enumerate() {
12572        let output = run_output(
12573            r#"let g = gen_enumerate(iter([10, 20, 30]))
12574print(gen_collect(g))"#,
12575        );
12576        assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12577    }
12578
12579    #[test]
12580    fn test_vm_combinator_chaining() {
12581        let output = run_output(
12582            r#"fn naturals() {
12583    let mut n = 0
12584    while true {
12585        yield n
12586        n = n + 1
12587    }
12588}
12589let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12590print(result)"#,
12591        );
12592        assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12593    }
12594
12595    #[test]
12596    fn test_vm_for_over_take() {
12597        let output = run_output(
12598            r#"fn naturals() {
12599    let mut n = 0
12600    while true {
12601        yield n
12602        n = n + 1
12603    }
12604}
12605for x in take(naturals(), 5) {
12606    print(x)
12607}"#,
12608        );
12609        assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12610    }
12611
12612    #[test]
12613    fn test_vm_generator_error_propagation() {
12614        let result = run(r#"fn bad_gen() {
12615    yield 1
12616    throw "oops"
12617}
12618let g = bad_gen()
12619let mut caught = ""
12620next(g)
12621try {
12622    next(g)
12623} catch e {
12624    caught = e
12625}
12626print(caught)"#);
12627        // Should succeed (error caught)
12628        assert!(result.is_ok());
12629    }
12630
12631    #[test]
12632    fn test_vm_fibonacci_generator() {
12633        let output = run_output(
12634            r#"fn fib() {
12635    let mut a = 0
12636    let mut b = 1
12637    while true {
12638        yield a
12639        let temp = a + b
12640        a = b
12641        b = temp
12642    }
12643}
12644print(gen_collect(take(fib(), 8)))"#,
12645        );
12646        assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12647    }
12648
12649    #[test]
12650    fn test_vm_generator_method_syntax() {
12651        let output = run_output(
12652            r#"fn gen() {
12653    yield 1
12654    yield 2
12655    yield 3
12656}
12657let g = gen()
12658print(type_of(g))"#,
12659        );
12660        assert_eq!(output, vec!["generator"]);
12661    }
12662
12663    // ── Phase 10: Result/Option + ? operator tests ──
12664
12665    #[test]
12666    fn test_vm_ok_err_builtins() {
12667        let output = run_output("let r = Ok(42)\nprint(r)");
12668        assert_eq!(output, vec!["Result::Ok(42)"]);
12669
12670        let output = run_output("let r = Err(\"fail\")\nprint(r)");
12671        assert_eq!(output, vec!["Result::Err(fail)"]);
12672    }
12673
12674    #[test]
12675    fn test_vm_is_ok_is_err() {
12676        let output = run_output("print(is_ok(Ok(42)))");
12677        assert_eq!(output, vec!["true"]);
12678        let output = run_output("print(is_err(Ok(42)))");
12679        assert_eq!(output, vec!["false"]);
12680        let output = run_output("print(is_ok(Err(\"fail\")))");
12681        assert_eq!(output, vec!["false"]);
12682        let output = run_output("print(is_err(Err(\"fail\")))");
12683        assert_eq!(output, vec!["true"]);
12684    }
12685
12686    #[test]
12687    fn test_vm_unwrap_ok() {
12688        let output = run_output("print(unwrap(Ok(42)))");
12689        assert_eq!(output, vec!["42"]);
12690    }
12691
12692    #[test]
12693    fn test_vm_unwrap_err_panics() {
12694        let result = run("unwrap(Err(\"fail\"))");
12695        assert!(result.is_err());
12696    }
12697
12698    #[test]
12699    fn test_vm_try_on_ok() {
12700        let output = run_output(
12701            r#"fn get_val() { Ok(42) }
12702fn process() { let v = get_val()? + 1
12703Ok(v) }
12704print(process())"#,
12705        );
12706        assert_eq!(output, vec!["Result::Ok(43)"]);
12707    }
12708
12709    #[test]
12710    fn test_vm_try_on_err_propagates() {
12711        let output = run_output(
12712            r#"fn failing() { Err("oops") }
12713fn process() { let v = failing()?
12714Ok(v) }
12715print(process())"#,
12716        );
12717        assert_eq!(output, vec!["Result::Err(oops)"]);
12718    }
12719
12720    #[test]
12721    fn test_vm_try_on_none_propagates() {
12722        let output = run_output(
12723            r#"fn get_none() { none }
12724fn process() { let v = get_none()?
1272542 }
12726print(process())"#,
12727        );
12728        assert_eq!(output, vec!["none"]);
12729    }
12730
12731    #[test]
12732    fn test_vm_try_passthrough() {
12733        // ? on a normal value should passthrough
12734        let output = run_output(
12735            r#"fn get_val() { 42 }
12736fn process() { let v = get_val()?
12737v + 1 }
12738print(process())"#,
12739        );
12740        assert_eq!(output, vec!["43"]);
12741    }
12742
12743    #[test]
12744    fn test_vm_result_match() {
12745        let output = run_output(
12746            r#"let r = Ok(42)
12747print(is_ok(r))
12748print(unwrap(r))"#,
12749        );
12750        assert_eq!(output, vec!["true", "42"]);
12751    }
12752
12753    #[test]
12754    fn test_vm_result_match_err() {
12755        let output = run_output(
12756            r#"let r = Err("fail")
12757print(is_err(r))
12758match r {
12759    Result::Err(e) => print("got error"),
12760    _ => print("no error")
12761}"#,
12762        );
12763        assert_eq!(output, vec!["true", "got error"]);
12764    }
12765
12766    // ── Set tests ──
12767
12768    #[test]
12769    fn test_vm_set_from_dedup() {
12770        let output = run_output(
12771            r#"let s = set_from([1, 2, 3, 2, 1])
12772print(len(s))
12773print(type_of(s))"#,
12774        );
12775        assert_eq!(output, vec!["3", "set"]);
12776    }
12777
12778    #[test]
12779    fn test_vm_set_add() {
12780        let output = run_output(
12781            r#"let s = set_from([1, 2])
12782let s2 = set_add(s, 3)
12783let s3 = set_add(s2, 2)
12784print(len(s2))
12785print(len(s3))"#,
12786        );
12787        assert_eq!(output, vec!["3", "3"]);
12788    }
12789
12790    #[test]
12791    fn test_vm_set_remove() {
12792        let output = run_output(
12793            r#"let s = set_from([1, 2, 3])
12794let s2 = set_remove(s, 2)
12795print(len(s2))
12796print(set_contains(s2, 2))"#,
12797        );
12798        assert_eq!(output, vec!["2", "false"]);
12799    }
12800
12801    #[test]
12802    fn test_vm_set_contains() {
12803        let output = run_output(
12804            r#"let s = set_from([1, 2, 3])
12805print(set_contains(s, 2))
12806print(set_contains(s, 5))"#,
12807        );
12808        assert_eq!(output, vec!["true", "false"]);
12809    }
12810
12811    #[test]
12812    fn test_vm_set_union() {
12813        let output = run_output(
12814            r#"let a = set_from([1, 2, 3])
12815let b = set_from([3, 4, 5])
12816let c = set_union(a, b)
12817print(len(c))"#,
12818        );
12819        assert_eq!(output, vec!["5"]);
12820    }
12821
12822    #[test]
12823    fn test_vm_set_intersection() {
12824        let output = run_output(
12825            r#"let a = set_from([1, 2, 3])
12826let b = set_from([2, 3, 4])
12827let c = set_intersection(a, b)
12828print(len(c))"#,
12829        );
12830        assert_eq!(output, vec!["2"]);
12831    }
12832
12833    #[test]
12834    fn test_vm_set_difference() {
12835        let output = run_output(
12836            r#"let a = set_from([1, 2, 3])
12837let b = set_from([2, 3, 4])
12838let c = set_difference(a, b)
12839print(len(c))"#,
12840        );
12841        assert_eq!(output, vec!["1"]);
12842    }
12843
12844    #[test]
12845    fn test_vm_set_for_loop() {
12846        let output = run_output(
12847            r#"let s = set_from([10, 20, 30])
12848let total = 0
12849for item in s {
12850    total = total + item
12851}
12852print(total)"#,
12853        );
12854        assert_eq!(output, vec!["60"]);
12855    }
12856
12857    #[test]
12858    fn test_vm_set_to_list() {
12859        let output = run_output(
12860            r#"let s = set_from([3, 1, 2])
12861let lst = s.to_list()
12862print(type_of(lst))
12863print(len(lst))"#,
12864        );
12865        assert_eq!(output, vec!["list", "3"]);
12866    }
12867
12868    #[test]
12869    fn test_vm_set_method_contains() {
12870        let output = run_output(
12871            r#"let s = set_from([1, 2, 3])
12872print(s.contains(2))
12873print(s.contains(5))"#,
12874        );
12875        assert_eq!(output, vec!["true", "false"]);
12876    }
12877
12878    #[test]
12879    fn test_vm_set_method_add_remove() {
12880        let output = run_output(
12881            r#"let s = set_from([1, 2])
12882let s2 = s.add(3)
12883print(s2.len())
12884let s3 = s2.remove(1)
12885print(s3.len())"#,
12886        );
12887        assert_eq!(output, vec!["3", "2"]);
12888    }
12889
12890    #[test]
12891    fn test_vm_set_method_union_intersection_difference() {
12892        let output = run_output(
12893            r#"let a = set_from([1, 2, 3])
12894let b = set_from([2, 3, 4])
12895print(a.union(b).len())
12896print(a.intersection(b).len())
12897print(a.difference(b).len())"#,
12898        );
12899        assert_eq!(output, vec!["4", "2", "1"]);
12900    }
12901
12902    #[test]
12903    fn test_vm_set_empty() {
12904        let output = run_output(
12905            r#"let s = set_from([])
12906print(len(s))
12907let s2 = s.add(1)
12908print(len(s2))"#,
12909        );
12910        assert_eq!(output, vec!["0", "1"]);
12911    }
12912
12913    #[test]
12914    fn test_vm_set_string_values() {
12915        let output = run_output(
12916            r#"let s = set_from(["a", "b", "a", "c"])
12917print(len(s))
12918print(s.contains("b"))"#,
12919        );
12920        assert_eq!(output, vec!["3", "true"]);
12921    }
12922
12923    // ── Phase 11: Module System VM Tests ──
12924
12925    #[test]
12926    fn test_vm_import_with_caching() {
12927        // Test that the VM has caching fields initialized
12928        let vm = Vm::new();
12929        assert!(vm.module_cache.is_empty());
12930        assert!(vm.importing_files.is_empty());
12931        assert!(vm.file_path.is_none());
12932    }
12933
12934    #[test]
12935    fn test_vm_use_single_file() {
12936        // Create a temp dir with module files
12937        let dir = tempfile::tempdir().unwrap();
12938        let lib_path = dir.path().join("math.tl");
12939        std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12940
12941        let main_path = dir.path().join("main.tl");
12942        std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12943
12944        let source = std::fs::read_to_string(&main_path).unwrap();
12945        let program = tl_parser::parse(&source).unwrap();
12946        let proto = crate::compiler::compile(&program).unwrap();
12947
12948        let mut vm = Vm::new();
12949        vm.file_path = Some(main_path.to_string_lossy().to_string());
12950        vm.execute(&proto).unwrap();
12951        assert_eq!(vm.output, vec!["3"]);
12952    }
12953
12954    #[test]
12955    fn test_vm_use_wildcard() {
12956        let dir = tempfile::tempdir().unwrap();
12957        std::fs::write(
12958            dir.path().join("helpers.tl"),
12959            "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12960        )
12961        .unwrap();
12962
12963        let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12964        let main_path = dir.path().join("main.tl");
12965        std::fs::write(&main_path, main_src).unwrap();
12966
12967        let program = tl_parser::parse(main_src).unwrap();
12968        let proto = crate::compiler::compile(&program).unwrap();
12969
12970        let mut vm = Vm::new();
12971        vm.file_path = Some(main_path.to_string_lossy().to_string());
12972        vm.execute(&proto).unwrap();
12973        assert_eq!(vm.output, vec!["hello", "bye"]);
12974    }
12975
12976    #[test]
12977    fn test_vm_use_aliased() {
12978        let dir = tempfile::tempdir().unwrap();
12979        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12980
12981        let main_src = "use mylib as m\nprint(m.compute())";
12982        let main_path = dir.path().join("main.tl");
12983        std::fs::write(&main_path, main_src).unwrap();
12984
12985        let program = tl_parser::parse(main_src).unwrap();
12986        let proto = crate::compiler::compile(&program).unwrap();
12987
12988        let mut vm = Vm::new();
12989        vm.file_path = Some(main_path.to_string_lossy().to_string());
12990        vm.execute(&proto).unwrap();
12991        assert_eq!(vm.output, vec!["42"]);
12992    }
12993
12994    #[test]
12995    fn test_vm_use_directory_module() {
12996        let dir = tempfile::tempdir().unwrap();
12997        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12998        std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12999
13000        let main_src = "use utils\nprint(helper())";
13001        let main_path = dir.path().join("main.tl");
13002        std::fs::write(&main_path, main_src).unwrap();
13003
13004        let program = tl_parser::parse(main_src).unwrap();
13005        let proto = crate::compiler::compile(&program).unwrap();
13006
13007        let mut vm = Vm::new();
13008        vm.file_path = Some(main_path.to_string_lossy().to_string());
13009        vm.execute(&proto).unwrap();
13010        assert_eq!(vm.output, vec!["99"]);
13011    }
13012
13013    #[test]
13014    fn test_vm_circular_import_detection() {
13015        let dir = tempfile::tempdir().unwrap();
13016        let a_path = dir.path().join("a.tl");
13017        let b_path = dir.path().join("b.tl");
13018        std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
13019        std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
13020
13021        let source = std::fs::read_to_string(&a_path).unwrap();
13022        let program = tl_parser::parse(&source).unwrap();
13023        let proto = crate::compiler::compile(&program).unwrap();
13024
13025        let mut vm = Vm::new();
13026        vm.file_path = Some(a_path.to_string_lossy().to_string());
13027        let result = vm.execute(&proto);
13028        assert!(result.is_err());
13029        assert!(format!("{:?}", result).contains("Circular import"));
13030    }
13031
13032    #[test]
13033    fn test_vm_module_caching() {
13034        // Import the same module twice — should use cache
13035        let dir = tempfile::tempdir().unwrap();
13036        std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
13037
13038        let main_src = "use cached\nuse cached\nprint(X)";
13039        let main_path = dir.path().join("main.tl");
13040        std::fs::write(&main_path, main_src).unwrap();
13041
13042        let program = tl_parser::parse(main_src).unwrap();
13043        let proto = crate::compiler::compile(&program).unwrap();
13044
13045        let mut vm = Vm::new();
13046        vm.file_path = Some(main_path.to_string_lossy().to_string());
13047        vm.execute(&proto).unwrap();
13048        assert_eq!(vm.output, vec!["42"]);
13049    }
13050
13051    #[test]
13052    fn test_vm_existing_import_still_works() {
13053        // Verify backward compat of classic import
13054        let dir = tempfile::tempdir().unwrap();
13055        let lib_path = dir.path().join("lib.tl");
13056        std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
13057
13058        let main_src = format!(
13059            "import \"{}\"\nprint(imported_fn())",
13060            lib_path.to_string_lossy()
13061        );
13062        let program = tl_parser::parse(&main_src).unwrap();
13063        let proto = crate::compiler::compile(&program).unwrap();
13064
13065        let mut vm = Vm::new();
13066        vm.execute(&proto).unwrap();
13067        assert_eq!(vm.output, vec!["123"]);
13068    }
13069
13070    #[test]
13071    fn test_vm_pub_fn_parsing() {
13072        // Pub fn should compile and run normally
13073        let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
13074        assert_eq!(output, vec!["3"]);
13075    }
13076
13077    #[test]
13078    fn test_vm_use_nested_path() {
13079        let dir = tempfile::tempdir().unwrap();
13080        std::fs::create_dir_all(dir.path().join("data")).unwrap();
13081        std::fs::write(
13082            dir.path().join("data/transforms.tl"),
13083            "fn clean(x) { x + 1 }",
13084        )
13085        .unwrap();
13086
13087        let main_src = "use data.transforms\nprint(clean(41))";
13088        let main_path = dir.path().join("main.tl");
13089        std::fs::write(&main_path, main_src).unwrap();
13090
13091        let program = tl_parser::parse(main_src).unwrap();
13092        let proto = crate::compiler::compile(&program).unwrap();
13093
13094        let mut vm = Vm::new();
13095        vm.file_path = Some(main_path.to_string_lossy().to_string());
13096        vm.execute(&proto).unwrap();
13097        assert_eq!(vm.output, vec!["42"]);
13098    }
13099
13100    // -- Integration tests: multi-file, backward compat, mixed --
13101
13102    #[test]
13103    fn test_integration_multi_file_use_functions() {
13104        // main.tl uses functions from lib.tl
13105        let dir = tempfile::tempdir().unwrap();
13106        std::fs::write(
13107            dir.path().join("lib.tl"),
13108            "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
13109        )
13110        .unwrap();
13111
13112        let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
13113        let main_path = dir.path().join("main.tl");
13114        std::fs::write(&main_path, main_src).unwrap();
13115
13116        let program = tl_parser::parse(main_src).unwrap();
13117        let proto = crate::compiler::compile(&program).unwrap();
13118        let mut vm = Vm::new();
13119        vm.file_path = Some(main_path.to_string_lossy().to_string());
13120        vm.execute(&proto).unwrap();
13121        assert_eq!(vm.output, vec!["Hello, World!", "42"]);
13122    }
13123
13124    #[test]
13125    fn test_integration_mixed_import_and_use() {
13126        // Combine classic import and use in same file
13127        let dir = tempfile::tempdir().unwrap();
13128        std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
13129        std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
13130
13131        let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
13132        let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
13133        let main_path = dir.path().join("main.tl");
13134        std::fs::write(&main_path, &main_src).unwrap();
13135
13136        let program = tl_parser::parse(&main_src).unwrap();
13137        let proto = crate::compiler::compile(&program).unwrap();
13138        let mut vm = Vm::new();
13139        vm.file_path = Some(main_path.to_string_lossy().to_string());
13140        vm.execute(&proto).unwrap();
13141        assert_eq!(vm.output, vec!["30"]);
13142    }
13143
13144    #[test]
13145    fn test_integration_directory_module_with_mod_tl() {
13146        // utils/mod.tl re-exports functions
13147        let dir = tempfile::tempdir().unwrap();
13148        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
13149        std::fs::write(
13150            dir.path().join("utils/mod.tl"),
13151            "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
13152        )
13153        .unwrap();
13154
13155        let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
13156        let main_path = dir.path().join("main.tl");
13157        std::fs::write(&main_path, main_src).unwrap();
13158
13159        let program = tl_parser::parse(main_src).unwrap();
13160        let proto = crate::compiler::compile(&program).unwrap();
13161        let mut vm = Vm::new();
13162        vm.file_path = Some(main_path.to_string_lossy().to_string());
13163        vm.execute(&proto).unwrap();
13164        assert_eq!(vm.output, vec!["99", "42!"]);
13165    }
13166
13167    #[test]
13168    fn test_integration_circular_dep_error() {
13169        let dir = tempfile::tempdir().unwrap();
13170        let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
13171        let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
13172        std::fs::write(
13173            dir.path().join("a.tl"),
13174            format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
13175        )
13176        .unwrap();
13177        std::fs::write(
13178            dir.path().join("b.tl"),
13179            format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
13180        )
13181        .unwrap();
13182
13183        let main_src = format!("import \"{a_abs}\"");
13184        let program = tl_parser::parse(&main_src).unwrap();
13185        let proto = crate::compiler::compile(&program).unwrap();
13186        let mut vm = Vm::new();
13187        let result = vm.execute(&proto);
13188        assert!(result.is_err());
13189        let err_msg = format!("{}", result.unwrap_err());
13190        assert!(
13191            err_msg.contains("Circular") || err_msg.contains("circular"),
13192            "Expected circular import error, got: {err_msg}"
13193        );
13194    }
13195
13196    #[test]
13197    fn test_integration_use_aliased_method_call() {
13198        // use lib as m, then m.compute()
13199        let dir = tempfile::tempdir().unwrap();
13200        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
13201
13202        let main_src = "use mylib as m\nprint(m.compute())";
13203        let main_path = dir.path().join("main.tl");
13204        std::fs::write(&main_path, main_src).unwrap();
13205
13206        let program = tl_parser::parse(main_src).unwrap();
13207        let proto = crate::compiler::compile(&program).unwrap();
13208        let mut vm = Vm::new();
13209        vm.file_path = Some(main_path.to_string_lossy().to_string());
13210        vm.execute(&proto).unwrap();
13211        assert_eq!(vm.output, vec!["42"]);
13212    }
13213
13214    #[test]
13215    fn test_integration_module_caching_shared() {
13216        // Import same module twice; second import uses cache, not re-execution
13217        let dir = tempfile::tempdir().unwrap();
13218        std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
13219
13220        let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
13221        let main_path = dir.path().join("main.tl");
13222        std::fs::write(&main_path, main_src).unwrap();
13223
13224        let program = tl_parser::parse(main_src).unwrap();
13225        let proto = crate::compiler::compile(&program).unwrap();
13226        let mut vm = Vm::new();
13227        vm.file_path = Some(main_path.to_string_lossy().to_string());
13228        vm.execute(&proto).unwrap();
13229        assert_eq!(vm.output, vec!["42", "42"]);
13230    }
13231
13232    #[test]
13233    fn test_integration_pub_keyword_in_module() {
13234        // pub fn in a module should work when imported
13235        let dir = tempfile::tempdir().unwrap();
13236        std::fs::write(
13237            dir.path().join("pubmod.tl"),
13238            "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
13239        )
13240        .unwrap();
13241
13242        let main_src = "use pubmod\nprint(public_fn())";
13243        let main_path = dir.path().join("main.tl");
13244        std::fs::write(&main_path, main_src).unwrap();
13245
13246        let program = tl_parser::parse(main_src).unwrap();
13247        let proto = crate::compiler::compile(&program).unwrap();
13248        let mut vm = Vm::new();
13249        vm.file_path = Some(main_path.to_string_lossy().to_string());
13250        vm.execute(&proto).unwrap();
13251        assert_eq!(vm.output, vec!["100"]);
13252    }
13253
13254    #[test]
13255    fn test_integration_backward_compat_import_as() {
13256        // Classic import-as syntax should still work
13257        let dir = tempfile::tempdir().unwrap();
13258        let lib_path = dir.path().join("mylib.tl");
13259        std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
13260
13261        let main_src = format!(
13262            "import \"{}\" as m\nprint(m.compute())",
13263            lib_path.to_string_lossy()
13264        );
13265        let program = tl_parser::parse(&main_src).unwrap();
13266        let proto = crate::compiler::compile(&program).unwrap();
13267        let mut vm = Vm::new();
13268        vm.execute(&proto).unwrap();
13269        assert_eq!(vm.output, vec!["77"]);
13270    }
13271
13272    // ── Phase 12: Generics & Traits (VM) ──────────────────
13273
13274    #[test]
13275    fn test_vm_generic_fn() {
13276        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
13277        assert_eq!(output, vec!["42"]);
13278    }
13279
13280    #[test]
13281    fn test_vm_generic_fn_string() {
13282        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
13283        assert_eq!(output, vec!["hello"]);
13284    }
13285
13286    #[test]
13287    fn test_vm_generic_struct() {
13288        let output = run_output(
13289            "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
13290        );
13291        assert_eq!(output, vec!["1", "hi"]);
13292    }
13293
13294    #[test]
13295    fn test_vm_trait_def_noop() {
13296        // Trait definitions should compile without error (no-op)
13297        let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
13298        assert_eq!(output, vec!["ok"]);
13299    }
13300
13301    #[test]
13302    fn test_vm_trait_impl_methods() {
13303        let output = run_output(
13304            "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())",
13305        );
13306        assert_eq!(output, vec!["point"]);
13307    }
13308
13309    #[test]
13310    fn test_vm_generic_enum() {
13311        // Generic enum declaration works — type params are erased at runtime
13312        let output = run_output(
13313            "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
13314        );
13315        assert_eq!(output, vec!["enum"]);
13316    }
13317
13318    #[test]
13319    fn test_vm_where_clause_runtime() {
13320        // Where clause is compile-time only; function still works at runtime
13321        let output =
13322            run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
13323        assert_eq!(output, vec!["10"]);
13324    }
13325
13326    #[test]
13327    fn test_vm_trait_impl_self_method() {
13328        let output = run_output(
13329            "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
13330        );
13331        assert_eq!(output, vec!["6"]);
13332    }
13333
13334    // ── Phase 12: Integration tests ──────────────────────────
13335
13336    #[test]
13337    fn test_vm_generic_fn_with_type_inference() {
13338        // Generic function called with different types
13339        let output = run_output(
13340            "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
13341        );
13342        assert_eq!(output, vec!["1", "a"]);
13343    }
13344
13345    #[test]
13346    fn test_vm_generic_struct_with_methods() {
13347        let output = run_output(
13348            "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
13349        );
13350        assert_eq!(output, vec!["42"]);
13351    }
13352
13353    #[test]
13354    fn test_vm_trait_def_impl_call() {
13355        let output = run_output(
13356            "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())",
13357        );
13358        assert_eq!(output, vec!["Alice"]);
13359    }
13360
13361    #[test]
13362    fn test_vm_multiple_generic_params() {
13363        let output = run_output(
13364            "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
13365        );
13366        assert_eq!(output, vec!["2"]);
13367    }
13368
13369    #[test]
13370    fn test_vm_backward_compat_non_generic() {
13371        // Existing non-generic code must still work unchanged
13372        let output = run_output(
13373            "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())",
13374        );
13375        assert_eq!(output, vec!["3", "7"]);
13376    }
13377
13378    // ── Phase 16: Package import resolution tests ──
13379
13380    #[test]
13381    fn test_vm_package_import_resolves() {
13382        // Create a test package on disk
13383        let tmp = tempfile::tempdir().unwrap();
13384        let pkg_dir = tmp.path().join("mylib");
13385        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13386        std::fs::write(
13387            pkg_dir.join("src/lib.tl"),
13388            "pub fn greet() { print(\"hello from pkg\") }",
13389        )
13390        .unwrap();
13391        std::fs::write(
13392            pkg_dir.join("tl.toml"),
13393            "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
13394        )
13395        .unwrap();
13396
13397        // use X imports all exports wildcard-style; call greet() directly
13398        let main_file = tmp.path().join("main.tl");
13399        std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
13400
13401        let source = std::fs::read_to_string(&main_file).unwrap();
13402        let program = tl_parser::parse(&source).unwrap();
13403        let proto = crate::compiler::compile(&program).unwrap();
13404
13405        let mut vm = Vm::new();
13406        vm.file_path = Some(main_file.to_string_lossy().to_string());
13407        vm.package_roots.insert("mylib".into(), pkg_dir);
13408        vm.execute(&proto).unwrap();
13409
13410        assert_eq!(vm.output, vec!["hello from pkg"]);
13411    }
13412
13413    #[test]
13414    fn test_vm_package_nested_import() {
13415        let tmp = tempfile::tempdir().unwrap();
13416        let pkg_dir = tmp.path().join("utils");
13417        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13418        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13419        std::fs::write(
13420            pkg_dir.join("tl.toml"),
13421            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13422        )
13423        .unwrap();
13424
13425        // use utils.math wildcard-imports math.tl contents
13426        let main_file = tmp.path().join("main.tl");
13427        std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
13428
13429        let source = std::fs::read_to_string(&main_file).unwrap();
13430        let program = tl_parser::parse(&source).unwrap();
13431        let proto = crate::compiler::compile(&program).unwrap();
13432
13433        let mut vm = Vm::new();
13434        vm.file_path = Some(main_file.to_string_lossy().to_string());
13435        vm.package_roots.insert("utils".into(), pkg_dir);
13436        vm.execute(&proto).unwrap();
13437
13438        assert_eq!(vm.output, vec!["42"]);
13439    }
13440
13441    #[test]
13442    fn test_vm_package_aliased_import() {
13443        let tmp = tempfile::tempdir().unwrap();
13444        let pkg_dir = tmp.path().join("utils");
13445        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13446        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13447        std::fs::write(
13448            pkg_dir.join("tl.toml"),
13449            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13450        )
13451        .unwrap();
13452
13453        // use X as Y creates a namespaced module object
13454        let main_file = tmp.path().join("main.tl");
13455        std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
13456
13457        let source = std::fs::read_to_string(&main_file).unwrap();
13458        let program = tl_parser::parse(&source).unwrap();
13459        let proto = crate::compiler::compile(&program).unwrap();
13460
13461        let mut vm = Vm::new();
13462        vm.file_path = Some(main_file.to_string_lossy().to_string());
13463        vm.package_roots.insert("utils".into(), pkg_dir);
13464        vm.execute(&proto).unwrap();
13465
13466        assert_eq!(vm.output, vec!["42"]);
13467    }
13468
13469    #[test]
13470    fn test_vm_package_underscore_to_hyphen() {
13471        let tmp = tempfile::tempdir().unwrap();
13472        let pkg_dir = tmp.path().join("my-pkg");
13473        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13474        std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
13475        std::fs::write(
13476            pkg_dir.join("tl.toml"),
13477            "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
13478        )
13479        .unwrap();
13480
13481        // TL identifiers use underscores, package names use hyphens
13482        let main_file = tmp.path().join("main.tl");
13483        std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
13484
13485        let source = std::fs::read_to_string(&main_file).unwrap();
13486        let program = tl_parser::parse(&source).unwrap();
13487        let proto = crate::compiler::compile(&program).unwrap();
13488
13489        let mut vm = Vm::new();
13490        vm.file_path = Some(main_file.to_string_lossy().to_string());
13491        vm.package_roots.insert("my-pkg".into(), pkg_dir);
13492        vm.execute(&proto).unwrap();
13493
13494        assert_eq!(vm.output, vec!["99"]);
13495    }
13496
13497    #[test]
13498    fn test_vm_local_module_priority_over_package() {
13499        // Local modules should take priority over packages
13500        let tmp = tempfile::tempdir().unwrap();
13501
13502        // Create a local module
13503        std::fs::write(
13504            tmp.path().join("mymod.tl"),
13505            "pub fn val() { print(\"local\") }",
13506        )
13507        .unwrap();
13508
13509        // Create a package with the same name
13510        let pkg_dir = tmp.path().join("pkg_mymod");
13511        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13512        std::fs::write(
13513            pkg_dir.join("src/lib.tl"),
13514            "pub fn val() { print(\"package\") }",
13515        )
13516        .unwrap();
13517
13518        // use mymod → wildcard imports, val() available directly
13519        let main_file = tmp.path().join("main.tl");
13520        std::fs::write(&main_file, "use mymod\nval()").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("mymod".into(), pkg_dir);
13529        vm.execute(&proto).unwrap();
13530
13531        // Local module should win
13532        assert_eq!(vm.output, vec!["local"]);
13533    }
13534
13535    #[test]
13536    fn test_vm_package_missing_error() {
13537        let tmp = tempfile::tempdir().unwrap();
13538        let main_file = tmp.path().join("main.tl");
13539        std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
13540
13541        let source = std::fs::read_to_string(&main_file).unwrap();
13542        let program = tl_parser::parse(&source).unwrap();
13543        let proto = crate::compiler::compile(&program).unwrap();
13544
13545        let mut vm = Vm::new();
13546        vm.file_path = Some(main_file.to_string_lossy().to_string());
13547        let result = vm.execute(&proto);
13548
13549        assert!(result.is_err());
13550        let err = format!("{:?}", result.unwrap_err());
13551        assert!(err.contains("Module not found"));
13552    }
13553
13554    #[test]
13555    #[cfg(feature = "native")]
13556    fn test_resolve_package_file_entry_points() {
13557        let tmp = tempfile::tempdir().unwrap();
13558
13559        // Test src/lib.tl entry point
13560        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
13561        std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
13562        let result = resolve_package_file(tmp.path(), &[]);
13563        assert!(result.is_some());
13564        assert!(result.unwrap().contains("lib.tl"));
13565
13566        // Test nested module in src/
13567        std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
13568        let result = resolve_package_file(tmp.path(), &["math"]);
13569        assert!(result.is_some());
13570        assert!(result.unwrap().contains("math.tl"));
13571    }
13572
13573    #[test]
13574    fn test_vm_package_propagates_to_sub_imports() {
13575        // Package roots should be available in sub-VM during imports
13576        let tmp = tempfile::tempdir().unwrap();
13577
13578        // Create a package
13579        let pkg_dir = tmp.path().join("helpers");
13580        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13581        std::fs::write(
13582            pkg_dir.join("src/lib.tl"),
13583            "pub fn help() { print(\"helped\") }",
13584        )
13585        .unwrap();
13586        std::fs::write(
13587            pkg_dir.join("tl.toml"),
13588            "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13589        )
13590        .unwrap();
13591
13592        // Create a local module that imports from the package (wildcard then calls directly)
13593        std::fs::write(
13594            tmp.path().join("bridge.tl"),
13595            "use helpers\npub fn run() { help() }",
13596        )
13597        .unwrap();
13598
13599        // use bridge wildcard-imports run(), then call it
13600        let main_file = tmp.path().join("main.tl");
13601        std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13602
13603        let source = std::fs::read_to_string(&main_file).unwrap();
13604        let program = tl_parser::parse(&source).unwrap();
13605        let proto = crate::compiler::compile(&program).unwrap();
13606
13607        let mut vm = Vm::new();
13608        vm.file_path = Some(main_file.to_string_lossy().to_string());
13609        vm.package_roots.insert("helpers".into(), pkg_dir);
13610        vm.execute(&proto).unwrap();
13611
13612        assert_eq!(vm.output, vec!["helped"]);
13613    }
13614
13615    // ── Phase 18: Closures & Lambdas Improvements ────────────────
13616
13617    #[test]
13618    fn test_block_body_closure_basic() {
13619        let output =
13620            run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13621        assert_eq!(output, vec!["11"]);
13622    }
13623
13624    #[test]
13625    fn test_block_body_closure_captures_upvalue() {
13626        let output = run_output(
13627            "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13628        );
13629        assert_eq!(output, vec!["15"]);
13630    }
13631
13632    #[test]
13633    fn test_block_body_closure_as_hof_arg() {
13634        let output = run_output(
13635            "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13636        );
13637        assert_eq!(output, vec!["[3, 5, 7]"]);
13638    }
13639
13640    #[test]
13641    fn test_block_body_closure_multi_stmt() {
13642        let output = run_output(
13643            "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13644        );
13645        assert_eq!(output, vec!["19"]);
13646    }
13647
13648    #[test]
13649    fn test_type_alias_noop() {
13650        // Type alias should be a no-op at runtime, code using aliased types should work
13651        let output = run_output(
13652            "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13653        );
13654        assert_eq!(output, vec!["10"]);
13655    }
13656
13657    #[test]
13658    fn test_type_alias_in_function_sig() {
13659        let output = run_output(
13660            "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13661        );
13662        assert_eq!(output, vec!["15"]);
13663    }
13664
13665    #[test]
13666    fn test_shorthand_closure() {
13667        let output = run_output("let double = x => x * 2\nprint(double(5))");
13668        assert_eq!(output, vec!["10"]);
13669    }
13670
13671    #[test]
13672    fn test_shorthand_closure_in_map() {
13673        let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13674        assert_eq!(output, vec!["[2, 4, 6]"]);
13675    }
13676
13677    #[test]
13678    fn test_iife() {
13679        let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13680        assert_eq!(output, vec!["10"]);
13681    }
13682
13683    #[test]
13684    fn test_hof_apply() {
13685        let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13686        assert_eq!(output, vec!["15"]);
13687    }
13688
13689    #[test]
13690    fn test_closure_stored_in_list() {
13691        let output = run_output(
13692            "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13693        );
13694        assert_eq!(output, vec!["6", "10"]);
13695    }
13696
13697    #[test]
13698    fn test_block_body_closure_with_return() {
13699        // Use explicit return statements since if/else is a statement, not a tail expression
13700        let output = run_output(
13701            "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13702        );
13703        assert_eq!(output, vec!["positive", "non-positive"]);
13704    }
13705
13706    #[test]
13707    fn test_shorthand_closure_in_filter() {
13708        let output = run_output(
13709            "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13710        );
13711        assert_eq!(output, vec!["[2, 4, 6]"]);
13712    }
13713
13714    #[test]
13715    fn test_block_closure_with_multiple_returns() {
13716        let output = run_output(
13717            "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13718        );
13719        assert_eq!(output, vec!["5", "3"]);
13720    }
13721
13722    #[test]
13723    fn test_type_alias_with_block_closure() {
13724        let output = run_output(
13725            "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13726        );
13727        assert_eq!(output, vec!["10"]);
13728    }
13729
13730    #[test]
13731    fn test_closure_both_backends_expr() {
13732        // Same test, just verify VM works correctly
13733        let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13734        assert_eq!(output, vec!["13"]);
13735    }
13736
13737    // Phase 20: Python FFI feature-disabled test
13738    #[test]
13739    #[cfg(not(feature = "python"))]
13740    fn test_py_feature_disabled() {
13741        let result = run("py_import(\"math\")");
13742        assert!(result.is_err());
13743        let msg = format!("{}", result.unwrap_err());
13744        assert!(msg.contains("python") && msg.contains("feature"));
13745    }
13746
13747    #[test]
13748    #[cfg(feature = "python")]
13749    fn test_vm_py_import_and_eval() {
13750        pyo3::prepare_freethreaded_python();
13751        let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13752        assert_eq!(output.len(), 1);
13753        let pi: f64 = output[0].parse().unwrap();
13754        assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13755    }
13756
13757    #[test]
13758    #[cfg(feature = "python")]
13759    fn test_vm_py_eval_arithmetic() {
13760        pyo3::prepare_freethreaded_python();
13761        let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13762        assert_eq!(output, vec!["1024"]);
13763    }
13764
13765    #[test]
13766    #[cfg(feature = "python")]
13767    fn test_vm_py_method_dispatch() {
13768        pyo3::prepare_freethreaded_python();
13769        let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13770        assert_eq!(output, vec!["5.0"]);
13771    }
13772
13773    #[test]
13774    #[cfg(feature = "python")]
13775    fn test_vm_py_list_conversion() {
13776        pyo3::prepare_freethreaded_python();
13777        let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13778        assert_eq!(output, vec!["[10, 20, 30]"]);
13779    }
13780
13781    #[test]
13782    #[cfg(feature = "python")]
13783    fn test_vm_py_none_conversion() {
13784        pyo3::prepare_freethreaded_python();
13785        let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13786        assert_eq!(output, vec!["none"]);
13787    }
13788
13789    #[test]
13790    #[cfg(feature = "python")]
13791    fn test_vm_py_error_msg_quality() {
13792        pyo3::prepare_freethreaded_python();
13793        let result = run("py_import(\"nonexistent_xyz_module\")");
13794        assert!(result.is_err());
13795        let msg = format!("{}", result.unwrap_err());
13796        assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13797    }
13798
13799    #[test]
13800    #[cfg(feature = "python")]
13801    fn test_vm_py_getattr_setattr() {
13802        pyo3::prepare_freethreaded_python();
13803        let output = run_output(
13804            "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13805        );
13806        assert_eq!(output, vec!["99"]);
13807    }
13808
13809    #[test]
13810    #[cfg(feature = "python")]
13811    fn test_vm_py_callable_round_trip() {
13812        pyo3::prepare_freethreaded_python();
13813        let output = run_output(
13814            "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13815        );
13816        assert_eq!(output, vec!["3"]);
13817    }
13818
13819    // ── Phase 21: Schema Evolution VM tests ──
13820
13821    #[test]
13822    fn test_vm_schema_register_and_get() {
13823        let source = r#"let fields = map_from("id", "int64", "name", "string")
13824schema_register("User", 1, fields)
13825let result = schema_get("User", 1)
13826print(len(result))"#;
13827        let output = run_output(source);
13828        assert_eq!(output, vec!["2"]);
13829    }
13830
13831    #[test]
13832    fn test_vm_schema_latest() {
13833        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13834schema_register("User", 2, map_from("id", "int64", "name", "string"))
13835let latest = schema_latest("User")
13836print(latest)"#;
13837        let output = run_output(source);
13838        assert_eq!(output, vec!["2"]);
13839    }
13840
13841    #[test]
13842    fn test_vm_schema_history() {
13843        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13844schema_register("User", 2, map_from("id", "int64", "name", "string"))
13845let hist = schema_history("User")
13846print(len(hist))"#;
13847        let output = run_output(source);
13848        assert_eq!(output, vec!["2"]);
13849    }
13850
13851    #[test]
13852    fn test_vm_schema_check_backward_compat() {
13853        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13854schema_register("User", 2, map_from("id", "int64", "name", "string"))
13855let issues = schema_check("User", 1, 2, "backward")
13856print(len(issues))"#;
13857        let output = run_output(source);
13858        assert_eq!(output, vec!["0"]);
13859    }
13860
13861    #[test]
13862    fn test_vm_schema_diff() {
13863        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13864schema_register("User", 2, map_from("id", "int64", "name", "string"))
13865let diffs = schema_diff("User", 1, 2)
13866print(len(diffs))"#;
13867        let output = run_output(source);
13868        assert_eq!(output, vec!["1"]);
13869    }
13870
13871    #[test]
13872    fn test_vm_schema_versions() {
13873        let source = r#"schema_register("T", 1, map_from("id", "int64"))
13874schema_register("T", 3, map_from("id", "int64"))
13875schema_register("T", 2, map_from("id", "int64"))
13876let vers = schema_versions("T")
13877print(len(vers))"#;
13878        let output = run_output(source);
13879        assert_eq!(output, vec!["3"]);
13880    }
13881
13882    #[test]
13883    fn test_vm_schema_fields() {
13884        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13885let fields = schema_fields("User", 1)
13886print(len(fields))"#;
13887        let output = run_output(source);
13888        assert_eq!(output, vec!["2"]);
13889    }
13890
13891    #[test]
13892    fn test_vm_compile_versioned_schema() {
13893        let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13894        let output = run_output(source);
13895        assert!(output[0].contains("__schema__:User:v1:"));
13896    }
13897
13898    #[test]
13899    fn test_vm_compile_migrate() {
13900        let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13901        let output = run_output(source);
13902        assert_eq!(output, vec!["ok"]);
13903    }
13904
13905    #[test]
13906    fn test_vm_schema_check_backward_compat_fails() {
13907        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13908schema_register("User", 2, map_from("id", "int64"))
13909let issues = schema_check("User", 1, 2, "backward")
13910print(len(issues))"#;
13911        let output = run_output(source);
13912        assert_eq!(output, vec!["1"]);
13913    }
13914
13915    // ── Phase 22: Decimal VM Tests ─────────────────────────────────
13916
13917    #[test]
13918    fn test_vm_decimal_literal_and_arithmetic() {
13919        let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13920        assert_eq!(output, vec!["13.0", "26.25"]);
13921    }
13922
13923    #[test]
13924    fn test_vm_decimal_div_by_zero() {
13925        let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13926        let program = tl_parser::parse(source).unwrap();
13927        let proto = crate::compile(&program).unwrap();
13928        let mut vm = Vm::new();
13929        let result = vm.execute(&proto);
13930        assert!(result.is_err());
13931    }
13932
13933    #[test]
13934    fn test_vm_decimal_comparison_ops() {
13935        let output =
13936            run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13937        assert_eq!(output, vec!["true", "false", "true"]);
13938    }
13939
13940    // ── Phase 23: Security VM Tests ────────────────────────────────
13941
13942    #[test]
13943    fn test_vm_secret_vault_crud() {
13944        let output = run_output(
13945            "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13946        );
13947        assert_eq!(output, vec!["***", "none"]);
13948    }
13949
13950    #[test]
13951    fn test_vm_mask_email_basic() {
13952        let output = run_output("print(mask_email(\"alice@domain.com\"))");
13953        assert_eq!(output, vec!["a***@domain.com"]);
13954    }
13955
13956    #[test]
13957    fn test_vm_mask_phone_basic() {
13958        let output = run_output("print(mask_phone(\"123-456-7890\"))");
13959        assert_eq!(output, vec!["***-***-7890"]);
13960    }
13961
13962    #[test]
13963    fn test_vm_mask_cc_basic() {
13964        let output = run_output("print(mask_cc(\"4111222233334444\"))");
13965        assert_eq!(output, vec!["****-****-****-4444"]);
13966    }
13967
13968    #[test]
13969    fn test_vm_hash_produces_hex() {
13970        let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13971        assert_eq!(output, vec!["64"]);
13972    }
13973
13974    #[test]
13975    fn test_vm_redact_modes() {
13976        let output =
13977            run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13978        assert_eq!(output, vec!["***", "h***o"]);
13979    }
13980
13981    #[test]
13982    fn test_vm_security_policy_sandbox() {
13983        let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13984        let program = tl_parser::parse(source).unwrap();
13985        let proto = crate::compile(&program).unwrap();
13986        let mut vm = Vm::new();
13987        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13988        vm.execute(&proto).unwrap();
13989        assert_eq!(vm.output, vec!["false", "true"]);
13990    }
13991
13992    // ── Phase 25: Async Runtime Tests (feature-gated) ──────────────
13993
13994    #[cfg(feature = "async-runtime")]
13995    #[test]
13996    fn test_vm_async_read_write_file() {
13997        let dir = tempfile::tempdir().unwrap();
13998        let path = dir.path().join("async_test.txt");
13999        let path_str = path.to_str().unwrap().replace('\\', "/");
14000        let source = format!(
14001            r#"let wt = async_write_file("{path_str}", "async hello")
14002let wr = await(wt)
14003let rt = async_read_file("{path_str}")
14004let content = await(rt)
14005print(content)"#
14006        );
14007        let output = run_output(&source);
14008        assert_eq!(output, vec!["async hello"]);
14009    }
14010
14011    #[cfg(feature = "async-runtime")]
14012    #[test]
14013    fn test_vm_async_sleep() {
14014        let source = r#"
14015let t = async_sleep(10)
14016let r = await(t)
14017print(r)
14018"#;
14019        let output = run_output(source);
14020        assert_eq!(output, vec!["none"]);
14021    }
14022
14023    #[cfg(feature = "async-runtime")]
14024    #[test]
14025    fn test_vm_select_first_wins() {
14026        // select between a fast sleep and a slow sleep — fast one wins
14027        let source = r#"
14028let fast = async_sleep(10)
14029let slow = async_sleep(5000)
14030let winner = select(fast, slow)
14031let result = await(winner)
14032print(result)
14033"#;
14034        let output = run_output(source);
14035        assert_eq!(output, vec!["none"]);
14036    }
14037
14038    #[cfg(feature = "async-runtime")]
14039    #[test]
14040    fn test_vm_race_all() {
14041        let source = r#"
14042let t1 = async_sleep(10)
14043let t2 = async_sleep(5000)
14044let winner = race_all([t1, t2])
14045let result = await(winner)
14046print(result)
14047"#;
14048        let output = run_output(source);
14049        assert_eq!(output, vec!["none"]);
14050    }
14051
14052    #[cfg(feature = "async-runtime")]
14053    #[test]
14054    fn test_vm_async_map() {
14055        let source = r#"
14056let items = [1, 2, 3]
14057let t = async_map(items, (x) => x * 10)
14058let result = await(t)
14059print(result)
14060"#;
14061        let output = run_output(source);
14062        assert_eq!(output, vec!["[10, 20, 30]"]);
14063    }
14064
14065    #[cfg(feature = "async-runtime")]
14066    #[test]
14067    fn test_vm_async_filter() {
14068        let source = r#"
14069let items = [1, 2, 3, 4, 5]
14070let t = async_filter(items, (x) => x > 3)
14071let result = await(t)
14072print(result)
14073"#;
14074        let output = run_output(source);
14075        assert_eq!(output, vec!["[4, 5]"]);
14076    }
14077
14078    #[cfg(feature = "async-runtime")]
14079    #[test]
14080    fn test_vm_async_write_file_returns_none() {
14081        let dir = tempfile::tempdir().unwrap();
14082        let path = dir.path().join("write_test.txt");
14083        let path_str = path.to_str().unwrap().replace('\\', "/");
14084        let source = format!(
14085            r#"let t = async_write_file("{path_str}", "test data")
14086let r = await(t)
14087print(r)"#
14088        );
14089        let output = run_output(&source);
14090        assert_eq!(output, vec!["none"]);
14091    }
14092
14093    #[cfg(feature = "async-runtime")]
14094    #[test]
14095    fn test_vm_async_security_policy_blocks_write() {
14096        let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
14097        let program = tl_parser::parse(source).unwrap();
14098        let proto = crate::compile(&program).unwrap();
14099        let mut vm = Vm::new();
14100        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14101        let result = vm.execute(&proto);
14102        assert!(result.is_err());
14103        let err = format!("{}", result.unwrap_err());
14104        assert!(
14105            err.contains("file_write not allowed"),
14106            "Expected security error, got: {err}"
14107        );
14108    }
14109
14110    #[cfg(feature = "async-runtime")]
14111    #[test]
14112    fn test_vm_async_security_policy_allows_read() {
14113        // Sandbox allows file_read, so async_read_file should succeed (even if file doesn't exist)
14114        let dir = tempfile::tempdir().unwrap();
14115        let path = dir.path().join("readable.txt");
14116        std::fs::write(&path, "safe content").unwrap();
14117        let path_str = path.to_str().unwrap().replace('\\', "/");
14118        let source = format!(
14119            r#"let t = async_read_file("{path_str}")
14120let r = await(t)
14121print(r)"#
14122        );
14123        let program = tl_parser::parse(&source).unwrap();
14124        let proto = crate::compile(&program).unwrap();
14125        let mut vm = Vm::new();
14126        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14127        vm.execute(&proto).unwrap();
14128        assert_eq!(vm.output, vec!["safe content"]);
14129    }
14130
14131    #[cfg(feature = "async-runtime")]
14132    #[test]
14133    fn test_vm_async_map_empty_list() {
14134        let source = r#"
14135let t = async_map([], (x) => x * 2)
14136let result = await(t)
14137print(result)
14138"#;
14139        let output = run_output(source);
14140        assert_eq!(output, vec!["[]"]);
14141    }
14142
14143    #[cfg(feature = "async-runtime")]
14144    #[test]
14145    fn test_vm_async_filter_none_match() {
14146        let source = r#"
14147let t = async_filter([1, 2, 3], (x) => x > 100)
14148let result = await(t)
14149print(result)
14150"#;
14151        let output = run_output(source);
14152        assert_eq!(output, vec!["[]"]);
14153    }
14154
14155    // --- Phase 26: Closure upvalue closing tests ---
14156
14157    #[test]
14158    fn test_vm_closure_returned_from_function() {
14159        let output = run_output(
14160            r#"
14161fn make_adder(n) {
14162    return (x) => x + n
14163}
14164let add5 = make_adder(5)
14165print(add5(3))
14166print(add5(10))
14167"#,
14168        );
14169        assert_eq!(output, vec!["8", "15"]);
14170    }
14171
14172    #[test]
14173    fn test_vm_closure_factory_multiple_calls() {
14174        let output = run_output(
14175            r#"
14176fn make_adder(n) {
14177    return (x) => x + n
14178}
14179let add2 = make_adder(2)
14180let add10 = make_adder(10)
14181print(add2(5))
14182print(add10(5))
14183print(add2(1))
14184"#,
14185        );
14186        assert_eq!(output, vec!["7", "15", "3"]);
14187    }
14188
14189    #[test]
14190    fn test_vm_closure_returned_in_list() {
14191        let output = run_output(
14192            r#"
14193fn make_ops(n) {
14194    let add = (x) => x + n
14195    let mul = (x) => x * n
14196    return [add, mul]
14197}
14198let ops = make_ops(3)
14199print(ops[0](10))
14200print(ops[1](10))
14201"#,
14202        );
14203        assert_eq!(output, vec!["13", "30"]);
14204    }
14205
14206    #[test]
14207    fn test_vm_nested_closure_return() {
14208        let output = run_output(
14209            r#"
14210fn outer(a) {
14211    fn inner(b) {
14212        return (x) => x + a + b
14213    }
14214    return inner(10)
14215}
14216let f = outer(5)
14217print(f(1))
14218"#,
14219        );
14220        assert_eq!(output, vec!["16"]);
14221    }
14222
14223    #[test]
14224    fn test_vm_multiple_closures_same_local() {
14225        let output = run_output(
14226            r#"
14227fn make_pair(n) {
14228    let inc = (x) => x + n
14229    let dec = (x) => x - n
14230    return [inc, dec]
14231}
14232let pair = make_pair(7)
14233print(pair[0](10))
14234print(pair[1](10))
14235"#,
14236        );
14237        assert_eq!(output, vec!["17", "3"]);
14238    }
14239
14240    #[test]
14241    fn test_vm_closure_captures_multiple_locals() {
14242        let output = run_output(
14243            r#"
14244fn make_greeter(greeting, name) {
14245    let sep = " "
14246    return () => greeting + sep + name
14247}
14248let hi = make_greeter("Hello", "World")
14249let bye = make_greeter("Goodbye", "Alice")
14250print(hi())
14251print(bye())
14252"#,
14253        );
14254        assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
14255    }
14256
14257    // ── Phase 27: Data Error Hierarchy tests ──
14258
14259    #[test]
14260    fn test_vm_throw_catch_preserves_enum() {
14261        let output = run_output(
14262            r#"
14263enum Color { Red, Green(x) }
14264try {
14265    throw Color::Green(42)
14266} catch e {
14267    match e {
14268        Color::Green(x) => print(x),
14269        _ => print("no match"),
14270    }
14271}
14272"#,
14273        );
14274        assert_eq!(output, vec!["42"]);
14275    }
14276
14277    #[test]
14278    fn test_vm_throw_catch_string_compat() {
14279        let output = run_output(
14280            r#"
14281try {
14282    throw "hello error"
14283} catch e {
14284    print(e)
14285}
14286"#,
14287        );
14288        assert_eq!(output, vec!["hello error"]);
14289    }
14290
14291    #[test]
14292    fn test_vm_runtime_error_still_string() {
14293        let output = run_output(
14294            r#"
14295try {
14296    let x = 1 / 0
14297} catch e {
14298    print(type_of(e))
14299}
14300"#,
14301        );
14302        assert_eq!(output, vec!["string"]);
14303    }
14304
14305    #[test]
14306    fn test_vm_data_error_construct_and_throw() {
14307        let output = run_output(
14308            r#"
14309try {
14310    throw DataError::ParseError("bad format", "file.csv")
14311} catch e {
14312    print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
14313    print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
14314}
14315"#,
14316        );
14317        assert_eq!(output, vec!["bad format", "file.csv"]);
14318    }
14319
14320    #[test]
14321    fn test_vm_network_error_construct() {
14322        let output = run_output(
14323            r#"
14324let err = NetworkError::TimeoutError("timed out")
14325match err {
14326    NetworkError::TimeoutError(msg) => print(msg),
14327    _ => print("no match"),
14328}
14329"#,
14330        );
14331        assert_eq!(output, vec!["timed out"]);
14332    }
14333
14334    #[test]
14335    fn test_vm_connector_error_construct() {
14336        let output = run_output(
14337            r#"
14338let err = ConnectorError::AuthError("invalid creds", "postgres")
14339print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
14340print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
14341"#,
14342        );
14343        assert_eq!(output, vec!["invalid creds", "postgres"]);
14344    }
14345
14346    #[test]
14347    fn test_vm_is_error_builtin() {
14348        let output = run_output(
14349            r#"
14350let e1 = DataError::NotFound("users")
14351let e2 = NetworkError::TimeoutError("slow")
14352let e3 = ConnectorError::ConfigError("bad", "redis")
14353let e4 = "not an error"
14354print(is_error(e1))
14355print(is_error(e2))
14356print(is_error(e3))
14357print(is_error(e4))
14358"#,
14359        );
14360        assert_eq!(output, vec!["true", "true", "true", "false"]);
14361    }
14362
14363    #[test]
14364    fn test_vm_error_type_builtin() {
14365        let output = run_output(
14366            r#"
14367let e1 = DataError::ParseError("bad", "x.csv")
14368let e2 = NetworkError::HttpError("fail", "url")
14369let e3 = "not an error"
14370print(error_type(e1))
14371print(error_type(e2))
14372print(error_type(e3))
14373"#,
14374        );
14375        assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
14376    }
14377
14378    #[test]
14379    fn test_vm_match_error_variants() {
14380        let output = run_output(
14381            r#"
14382fn handle(err) {
14383    print(match err {
14384        DataError::ParseError(msg, _) => "parse: " + msg,
14385        DataError::SchemaError(msg, _, _) => "schema: " + msg,
14386        DataError::ValidationError(_, field) => "validation: " + field,
14387        DataError::NotFound(name) => "not found: " + name,
14388        _ => "unknown"
14389    })
14390}
14391handle(DataError::ParseError("bad csv", "data.csv"))
14392handle(DataError::NotFound("users_table"))
14393handle(DataError::SchemaError("mismatch", "int", "string"))
14394handle(DataError::ValidationError("invalid", "email"))
14395"#,
14396        );
14397        assert_eq!(
14398            output,
14399            vec![
14400                "parse: bad csv",
14401                "not found: users_table",
14402                "schema: mismatch",
14403                "validation: email",
14404            ]
14405        );
14406    }
14407
14408    #[test]
14409    fn test_vm_rethrow_structured_error() {
14410        let output = run_output(
14411            r#"
14412try {
14413    try {
14414        throw DataError::NotFound("config")
14415    } catch e {
14416        throw e
14417    }
14418} catch outer {
14419    match outer {
14420        DataError::NotFound(name) => print("caught: " + name),
14421        _ => print("wrong type"),
14422    }
14423}
14424"#,
14425        );
14426        assert_eq!(output, vec!["caught: config"]);
14427    }
14428
14429    // ── Phase 28: Ownership & Move Semantics ──
14430
14431    #[test]
14432    fn test_vm_pipe_moves_value() {
14433        // x |> f() should consume x — accessing x after pipe gives error
14434        let result = run(r#"
14435fn identity(v) { v }
14436let x = [1, 2, 3]
14437x |> identity()
14438print(x)
14439"#);
14440        assert!(result.is_err());
14441        let err = result.unwrap_err().to_string();
14442        assert!(err.contains("moved"), "Error should mention 'moved': {err}");
14443    }
14444
14445    #[test]
14446    fn test_vm_clone_before_pipe() {
14447        // x.clone() |> f() should not consume x
14448        let output = run_output(
14449            r#"
14450fn identity(v) { v }
14451let x = [1, 2, 3]
14452x.clone() |> identity()
14453print(x)
14454"#,
14455        );
14456        assert_eq!(output, vec!["[1, 2, 3]"]);
14457    }
14458
14459    #[test]
14460    fn test_vm_clone_list_deep() {
14461        // Mutating a cloned list should not affect the original
14462        let output = run_output(
14463            r#"
14464let original = [1, 2, 3]
14465let copy = original.clone()
14466copy[0] = 99
14467print(original)
14468print(copy)
14469"#,
14470        );
14471        assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
14472    }
14473
14474    #[test]
14475    fn test_vm_clone_map() {
14476        let output = run_output(
14477            r#"
14478let m = map_from("a", 1, "b", 2)
14479let m2 = m.clone()
14480m2["a"] = 99
14481print(m)
14482print(m2)
14483"#,
14484        );
14485        assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
14486    }
14487
14488    #[test]
14489    fn test_vm_clone_struct() {
14490        let output = run_output(
14491            r#"
14492struct Point { x: int64, y: int64 }
14493let p = Point { x: 1, y: 2 }
14494let p2 = p.clone()
14495print(p)
14496print(p2)
14497"#,
14498        );
14499        assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
14500    }
14501
14502    #[test]
14503    fn test_vm_ref_read_only() {
14504        // &x should be readable but not mutable
14505        let result = run(r#"
14506let x = [1, 2, 3]
14507let r = &x
14508r[0] = 99
14509"#);
14510        assert!(result.is_err());
14511        let err = result.unwrap_err().to_string();
14512        assert!(
14513            err.contains("Cannot mutate a borrowed reference"),
14514            "Error should mention reference: {err}"
14515        );
14516    }
14517
14518    #[test]
14519    fn test_vm_ref_transparent_read() {
14520        // Reading through a ref should work transparently
14521        let output = run_output(
14522            r#"
14523let x = [1, 2, 3]
14524let r = &x
14525print(len(r))
14526"#,
14527        );
14528        assert_eq!(output, vec!["3"]);
14529    }
14530
14531    #[test]
14532    fn test_vm_parallel_for_basic() {
14533        // parallel for should iterate all elements (runs sequentially in VM)
14534        let output = run_output(
14535            r#"
14536let items = [10, 20, 30]
14537parallel for item in items {
14538    print(item)
14539}
14540"#,
14541        );
14542        assert_eq!(output, vec!["10", "20", "30"]);
14543    }
14544
14545    #[test]
14546    fn test_vm_moved_value_clear_error() {
14547        // Error message should mention .clone()
14548        let result = run(r#"
14549fn f(x) { x }
14550let data = "hello"
14551data |> f()
14552print(data)
14553"#);
14554        assert!(result.is_err());
14555        let err = result.unwrap_err().to_string();
14556        assert!(
14557            err.contains("clone()"),
14558            "Error should suggest .clone(): {err}"
14559        );
14560    }
14561
14562    #[test]
14563    fn test_vm_reassign_after_move() {
14564        // After moving, reassigning the variable should work
14565        let output = run_output(
14566            r#"
14567fn f(x) { x }
14568let x = 1
14569x |> f()
14570let x = 2
14571print(x)
14572"#,
14573        );
14574        assert_eq!(output, vec!["2"]);
14575    }
14576
14577    #[test]
14578    fn test_vm_pipe_chain_move() {
14579        // Chained pipes should work — intermediate values don't need explicit binding
14580        let output = run_output(
14581            r#"
14582fn double(x) { x * 2 }
14583fn add_one(x) { x + 1 }
14584let result = 5 |> double() |> add_one()
14585print(result)
14586"#,
14587        );
14588        assert_eq!(output, vec!["11"]);
14589    }
14590
14591    #[test]
14592    fn test_vm_string_clone() {
14593        // .clone() on string values
14594        let output = run_output(
14595            r#"
14596let s = "hello"
14597let s2 = s.clone()
14598print(s)
14599print(s2)
14600"#,
14601        );
14602        assert_eq!(output, vec!["hello", "hello"]);
14603    }
14604
14605    #[test]
14606    fn test_vm_ref_method_dispatch() {
14607        // Methods should be callable through references
14608        let output = run_output(
14609            r#"
14610let s = "hello world"
14611let r = &s
14612print(r.len())
14613"#,
14614        );
14615        assert_eq!(output, vec!["11"]);
14616    }
14617
14618    #[test]
14619    fn test_vm_ref_member_access() {
14620        // Member access through ref should work
14621        let output = run_output(
14622            r#"
14623struct Point { x: int64, y: int64 }
14624let p = Point { x: 10, y: 20 }
14625let r = &p
14626print(r.x)
14627"#,
14628        );
14629        assert_eq!(output, vec!["10"]);
14630    }
14631
14632    #[test]
14633    fn test_vm_ref_set_member_blocked() {
14634        // Setting a member through a ref should fail
14635        let result = run(r#"
14636struct Point { x: int64, y: int64 }
14637let p = Point { x: 10, y: 20 }
14638let r = &p
14639r.x = 99
14640"#);
14641        assert!(result.is_err());
14642        let err = result.unwrap_err().to_string();
14643        assert!(
14644            err.contains("Cannot mutate a borrowed reference"),
14645            "Error: {err}"
14646        );
14647    }
14648
14649    // ── Phase 29: IR Integration Tests ──
14650
14651    #[test]
14652    fn test_ir_filter_merge_chain() {
14653        // Two adjacent filters should be merged by the IR optimizer
14654        let dir = tempfile::tempdir().unwrap();
14655        let csv = dir.path().join("data.csv");
14656        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14657        let src = format!(
14658            r#"let t = read_csv("{}")
14659let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14660print(r)"#,
14661            csv.to_str().unwrap()
14662        );
14663        let output = run_output(&src);
14664        // Both Alice(30) and Charlie(35) pass both filters
14665        assert!(
14666            output[0].contains("Alice"),
14667            "Output should contain Alice: {}",
14668            output[0]
14669        );
14670        assert!(
14671            output[0].contains("Charlie"),
14672            "Output should contain Charlie: {}",
14673            output[0]
14674        );
14675        assert!(
14676            !output[0].contains("Bob"),
14677            "Output should not contain Bob: {}",
14678            output[0]
14679        );
14680    }
14681
14682    #[test]
14683    fn test_ir_predicate_pushdown_through_select() {
14684        // filter after select should be pushed before select by IR optimizer
14685        let dir = tempfile::tempdir().unwrap();
14686        let csv = dir.path().join("data.csv");
14687        std::fs::write(
14688            &csv,
14689            "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14690        )
14691        .unwrap();
14692        let src = format!(
14693            r#"let t = read_csv("{}")
14694let r = t |> select(name, age) |> filter(age > 25) |> collect()
14695print(r)"#,
14696            csv.to_str().unwrap()
14697        );
14698        let output = run_output(&src);
14699        assert!(output[0].contains("Alice"), "Output should contain Alice");
14700        assert!(
14701            output[0].contains("Charlie"),
14702            "Output should contain Charlie"
14703        );
14704        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14705    }
14706
14707    #[test]
14708    fn test_ir_sort_filter_pushdown() {
14709        // filter after sort should be pushed before sort
14710        let dir = tempfile::tempdir().unwrap();
14711        let csv = dir.path().join("data.csv");
14712        std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14713        let src = format!(
14714            r#"let t = read_csv("{}")
14715let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14716print(r)"#,
14717            csv.to_str().unwrap()
14718        );
14719        let output = run_output(&src);
14720        assert!(output[0].contains("Alice"), "Output should contain Alice");
14721        assert!(
14722            output[0].contains("Charlie"),
14723            "Output should contain Charlie"
14724        );
14725        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14726    }
14727
14728    #[test]
14729    fn test_ir_multi_operation_chain() {
14730        // Complex chain: filter + select + sort + limit
14731        let dir = tempfile::tempdir().unwrap();
14732        let csv = dir.path().join("data.csv");
14733        std::fs::write(
14734            &csv,
14735            "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14736        )
14737        .unwrap();
14738        let src = format!(
14739            r#"let t = read_csv("{}")
14740let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14741print(r)"#,
14742            csv.to_str().unwrap()
14743        );
14744        let output = run_output(&src);
14745        // Top 2 by age descending among age>22: Charlie(35), Alice(30)
14746        assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14747        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14748    }
14749
14750    #[test]
14751    fn test_ir_pipe_move_semantics_preserved() {
14752        // The source variable should be moved after pipe chain
14753        let dir = tempfile::tempdir().unwrap();
14754        let csv = dir.path().join("data.csv");
14755        std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14756        let src = format!(
14757            r#"let t = read_csv("{}")
14758let r = t |> filter(age > 0) |> collect()
14759print(t)"#,
14760            csv.to_str().unwrap()
14761        );
14762        let result = run(&src);
14763        assert!(result.is_err(), "Should error on use-after-move");
14764    }
14765
14766    #[test]
14767    fn test_ir_non_table_op_fallback() {
14768        // A pipe chain with a non-table op should fall back to legacy path
14769        let output = run_output(
14770            r#"
14771fn double(x) { x * 2 }
14772let result = 5 |> double()
14773print(result)
14774"#,
14775        );
14776        assert_eq!(output, vec!["10"]);
14777    }
14778
14779    #[test]
14780    fn test_ir_mixed_pipe_fallback() {
14781        // A pipe into a builtin (not a table op) should use legacy path
14782        let output = run_output(
14783            r#"
14784let result = [3, 1, 2] |> len()
14785print(result)
14786"#,
14787        );
14788        assert_eq!(output, vec!["3"]);
14789    }
14790
14791    #[test]
14792    fn test_ir_single_filter_roundtrip() {
14793        // Even a single filter goes through IR and round-trips correctly
14794        let dir = tempfile::tempdir().unwrap();
14795        let csv = dir.path().join("data.csv");
14796        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14797        let src = format!(
14798            r#"let t = read_csv("{}")
14799let r = t |> filter(age > 25) |> collect()
14800print(r)"#,
14801            csv.to_str().unwrap()
14802        );
14803        let output = run_output(&src);
14804        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14805        assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14806    }
14807
14808    // ── Phase 34: Agent Framework ──
14809
14810    #[test]
14811    fn test_vm_agent_definition() {
14812        let output = run_output(
14813            r#"
14814fn search(query) { "found: " + query }
14815agent bot {
14816    model: "gpt-4o",
14817    system: "You are helpful.",
14818    tools {
14819        search: {
14820            description: "Search the web",
14821            parameters: {}
14822        }
14823    },
14824    max_turns: 5
14825}
14826print(type_of(bot))
14827print(bot)
14828"#,
14829        );
14830        assert_eq!(output, vec!["agent", "<agent bot>"]);
14831    }
14832
14833    #[test]
14834    fn test_vm_agent_minimal() {
14835        let output = run_output(
14836            r#"
14837agent minimal_bot {
14838    model: "claude-sonnet-4-20250514"
14839}
14840print(type_of(minimal_bot))
14841"#,
14842        );
14843        assert_eq!(output, vec!["agent"]);
14844    }
14845
14846    #[test]
14847    fn test_vm_agent_with_base_url() {
14848        let output = run_output(
14849            r#"
14850agent local_bot {
14851    model: "llama3",
14852    base_url: "http://localhost:11434/v1",
14853    max_turns: 3
14854}
14855print(local_bot)
14856"#,
14857        );
14858        assert_eq!(output, vec!["<agent local_bot>"]);
14859    }
14860
14861    #[test]
14862    fn test_vm_agent_multiple_tools() {
14863        let output = run_output(
14864            r#"
14865fn search(query) { "result" }
14866fn weather(city) { "sunny" }
14867agent helper {
14868    model: "gpt-4o",
14869    tools {
14870        search: { description: "Search", parameters: {} },
14871        weather: { description: "Get weather", parameters: {} }
14872    }
14873}
14874print(type_of(helper))
14875"#,
14876        );
14877        assert_eq!(output, vec!["agent"]);
14878    }
14879
14880    #[test]
14881    fn test_vm_agent_lifecycle_hooks_stored() {
14882        let output = run_output(
14883            r#"
14884fn search(q) { "result" }
14885agent bot {
14886    model: "gpt-4o",
14887    tools {
14888        search: { description: "Search", parameters: {} }
14889    },
14890    on_tool_call {
14891        println("tool: " + tool_name)
14892    }
14893    on_complete {
14894        println("done")
14895    }
14896}
14897print(type_of(bot))
14898print(type_of(__agent_bot_on_tool_call__))
14899print(type_of(__agent_bot_on_complete__))
14900"#,
14901        );
14902        assert_eq!(output, vec!["agent", "function", "function"]);
14903    }
14904
14905    #[test]
14906    fn test_vm_agent_lifecycle_hook_callable() {
14907        let output = run_output(
14908            r#"
14909agent bot {
14910    model: "gpt-4o",
14911    on_tool_call {
14912        println("called: " + tool_name + " -> " + tool_result)
14913    }
14914    on_complete {
14915        println("completed")
14916    }
14917}
14918__agent_bot_on_tool_call__("search", "query", "found it")
14919__agent_bot_on_complete__("hello")
14920"#,
14921        );
14922        assert_eq!(output, vec!["called: search -> found it", "completed"]);
14923    }
14924}