Skip to main content

tl_compiler/
vm.rs

1// ThinkingLanguage — Bytecode Virtual Machine
2// Register-based VM that executes compiled bytecode.
3
4use std::collections::HashMap;
5#[cfg(feature = "native")]
6use std::sync::mpsc;
7use std::sync::{Arc, Mutex, OnceLock};
8
9/// Global mutex for env_set/env_remove thread safety (std::env::set_var is not thread-safe).
10static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();
11fn env_lock() -> std::sync::MutexGuard<'static, ()> {
12    ENV_MUTEX
13        .get_or_init(|| Mutex::new(()))
14        .lock()
15        .unwrap_or_else(|e| e.into_inner())
16}
17#[cfg(feature = "native")]
18use std::time::Duration;
19
20#[cfg(feature = "native")]
21use rayon::prelude::*;
22use tl_ast::Expr as AstExpr;
23#[cfg(feature = "native")]
24use tl_data::datafusion::execution::FunctionRegistry;
25#[cfg(feature = "native")]
26use tl_data::translate::{LocalValue, TranslateContext, translate_expr};
27#[cfg(feature = "native")]
28use tl_data::{DataEngine, JoinType, col, lit};
29use tl_errors::{RuntimeError, TlError};
30
31use crate::chunk::*;
32use crate::opcode::*;
33use crate::value::*;
34
35fn decimal_to_f64(d: &rust_decimal::Decimal) -> f64 {
36    use rust_decimal::prelude::ToPrimitive;
37    d.to_f64().unwrap_or(f64::NAN)
38}
39
40fn runtime_err(msg: impl Into<String>) -> TlError {
41    TlError::Runtime(RuntimeError {
42        message: msg.into(),
43        span: None,
44        stack_trace: vec![],
45    })
46}
47
48/// Resolve a connection name via TL_CONFIG_PATH config file.
49/// If `name` looks like a connection string (contains `=` or `://`), return it as-is.
50/// Otherwise, look it up in the JSON config file at `TL_CONFIG_PATH` (or `./tl_config.json`).
51fn resolve_tl_config_connection(name: &str) -> String {
52    // If it already looks like a connection string, pass through
53    if name.contains('=') || name.contains("://") {
54        return name.to_string();
55    }
56    // Try to load config
57    let config_path =
58        std::env::var("TL_CONFIG_PATH").unwrap_or_else(|_| "tl_config.json".to_string());
59    let Ok(contents) = std::fs::read_to_string(&config_path) else {
60        return name.to_string();
61    };
62    let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
63        return name.to_string();
64    };
65    // Look up in "connections" object first, then top-level
66    if let Some(conn) = json
67        .get("connections")
68        .and_then(|c| c.get(name))
69        .and_then(|v| v.as_str())
70    {
71        return conn.to_string();
72    }
73    if let Some(conn) = json.get(name).and_then(|v| v.as_str()) {
74        return conn.to_string();
75    }
76    // Not found — return original (will fail at connection time with a clear error)
77    name.to_string()
78}
79
80/// Compare two VmValues for equality (used by set operations).
81fn vm_values_equal(a: &VmValue, b: &VmValue) -> bool {
82    match (a, b) {
83        (VmValue::Int(x), VmValue::Int(y)) => x == y,
84        (VmValue::Float(x), VmValue::Float(y)) => x == y,
85        (VmValue::String(x), VmValue::String(y)) => x == y,
86        (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
87        (VmValue::None, VmValue::None) => true,
88        _ => false,
89    }
90}
91
92#[cfg(feature = "native")]
93/// Resolve a file path within a package directory for package imports.
94/// `pkg_root` is the package root (containing tl.toml).
95/// `remaining` are the path segments after the package name.
96/// Entry point convention: src/lib.tl > src/mod.tl > src/main.tl > mod.tl > lib.tl
97fn resolve_package_file(pkg_root: &std::path::Path, remaining: &[&str]) -> Option<String> {
98    if remaining.is_empty() {
99        // Import the package itself — find entry point
100        let src = pkg_root.join("src");
101        for entry in &["lib.tl", "mod.tl", "main.tl"] {
102            let p = src.join(entry);
103            if p.exists() {
104                return Some(p.to_string_lossy().to_string());
105            }
106        }
107        for entry in &["mod.tl", "lib.tl"] {
108            let p = pkg_root.join(entry);
109            if p.exists() {
110                return Some(p.to_string_lossy().to_string());
111            }
112        }
113        return None;
114    }
115
116    // Try src/<remaining>.tl, then src/<remaining>/mod.tl
117    let rel = remaining.join("/");
118    let src = pkg_root.join("src");
119
120    let file_path = src.join(format!("{rel}.tl"));
121    if file_path.exists() {
122        return Some(file_path.to_string_lossy().to_string());
123    }
124
125    let dir_path = src.join(&rel).join("mod.tl");
126    if dir_path.exists() {
127        return Some(dir_path.to_string_lossy().to_string());
128    }
129
130    // Also try without src/ prefix
131    let file_path = pkg_root.join(format!("{rel}.tl"));
132    if file_path.exists() {
133        return Some(file_path.to_string_lossy().to_string());
134    }
135
136    let dir_path = pkg_root.join(&rel).join("mod.tl");
137    if dir_path.exists() {
138        return Some(dir_path.to_string_lossy().to_string());
139    }
140
141    // Parent fallback for item-within-module
142    if remaining.len() > 1 {
143        let parent = &remaining[..remaining.len() - 1];
144        let parent_rel = parent.join("/");
145        let parent_file = src.join(format!("{parent_rel}.tl"));
146        if parent_file.exists() {
147            return Some(parent_file.to_string_lossy().to_string());
148        }
149        let parent_file = pkg_root.join(format!("{parent_rel}.tl"));
150        if parent_file.exists() {
151            return Some(parent_file.to_string_lossy().to_string());
152        }
153    }
154
155    None
156}
157
158/// Convert serde_json::Value to VmValue
159fn vm_json_to_value(v: &serde_json::Value) -> VmValue {
160    match v {
161        serde_json::Value::Null => VmValue::None,
162        serde_json::Value::Bool(b) => VmValue::Bool(*b),
163        serde_json::Value::Number(n) => {
164            if let Some(i) = n.as_i64() {
165                VmValue::Int(i)
166            } else {
167                VmValue::Float(n.as_f64().unwrap_or(0.0))
168            }
169        }
170        serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
171        serde_json::Value::Array(arr) => {
172            VmValue::List(Box::new(arr.iter().map(vm_json_to_value).collect()))
173        }
174        serde_json::Value::Object(obj) => VmValue::Map(Box::new(
175            obj.iter()
176                .map(|(k, v)| (Arc::from(k.as_str()), vm_json_to_value(v)))
177                .collect(),
178        )),
179    }
180}
181
182/// Convert VmValue to serde_json::Value
183fn vm_value_to_json(v: &VmValue) -> serde_json::Value {
184    match v {
185        VmValue::None => serde_json::Value::Null,
186        VmValue::Bool(b) => serde_json::Value::Bool(*b),
187        VmValue::Int(n) => serde_json::json!(*n),
188        VmValue::Float(n) => serde_json::json!(*n),
189        VmValue::String(s) => serde_json::Value::String(s.to_string()),
190        VmValue::List(items) => {
191            serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
192        }
193        VmValue::Map(pairs) => {
194            let obj: serde_json::Map<String, serde_json::Value> = pairs
195                .iter()
196                .map(|(k, v)| (k.to_string(), vm_value_to_json(v)))
197                .collect();
198            serde_json::Value::Object(obj)
199        }
200        VmValue::Secret(_) => serde_json::Value::String("***".to_string()),
201        _ => serde_json::Value::String(format!("{v}")),
202    }
203}
204
205/// Minimum list size before we attempt parallel execution.
206#[cfg(feature = "native")]
207const PARALLEL_THRESHOLD: usize = 10_000;
208
209/// Check if a closure is pure (no captured upvalues) and thus safe to run in parallel.
210#[cfg(feature = "native")]
211fn is_pure_closure(func: &VmValue) -> bool {
212    match func {
213        VmValue::Function(closure) => closure.upvalues.is_empty(),
214        _ => false,
215    }
216}
217
218/// Execute a pure function (no upvalues) in an isolated mini-VM.
219/// Used by rayon parallel operations — each thread gets its own stack.
220#[cfg(feature = "native")]
221fn execute_pure_fn(proto: &Arc<Prototype>, args: &[VmValue]) -> Result<VmValue, TlError> {
222    let base = 0;
223    let num_regs = proto.num_registers as usize;
224    let mut stack = vec![VmValue::None; num_regs + 1];
225    for (i, arg) in args.iter().enumerate() {
226        stack[i] = arg.clone();
227    }
228
229    let mut ip = 0;
230    loop {
231        if ip >= proto.code.len() {
232            return Ok(VmValue::None);
233        }
234        let inst = proto.code[ip];
235        let op = decode_op(inst);
236        let a = decode_a(inst);
237        let b = decode_b(inst);
238        let c = decode_c(inst);
239        let bx = decode_bx(inst);
240        let sbx = decode_sbx(inst);
241
242        ip += 1;
243
244        match op {
245            Op::LoadConst => {
246                let val = match &proto.constants[bx as usize] {
247                    Constant::Int(n) => VmValue::Int(*n),
248                    Constant::Float(n) => VmValue::Float(*n),
249                    Constant::String(s) => VmValue::String(s.clone()),
250                    Constant::Decimal(s) => {
251                        use std::str::FromStr;
252                        VmValue::Decimal(rust_decimal::Decimal::from_str(s).unwrap_or_default())
253                    }
254                    _ => VmValue::None,
255                };
256                stack[base + a as usize] = val;
257            }
258            Op::LoadNone => stack[base + a as usize] = VmValue::None,
259            Op::LoadTrue => stack[base + a as usize] = VmValue::Bool(true),
260            Op::LoadFalse => stack[base + a as usize] = VmValue::Bool(false),
261            Op::Move | Op::GetLocal => {
262                let val = stack[base + b as usize].clone();
263                stack[base + a as usize] = val;
264            }
265            Op::SetLocal => {
266                let val = stack[base + a as usize].clone();
267                stack[base + b as usize] = val;
268            }
269            Op::Add => {
270                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
271                    (VmValue::Int(x), VmValue::Int(y)) => x
272                        .checked_add(*y)
273                        .map(VmValue::Int)
274                        .unwrap_or_else(|| VmValue::Float(*x as f64 + *y as f64)),
275                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x + y),
276                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 + y),
277                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x + *y as f64),
278                    _ => return Err(runtime_err("Cannot add in parallel fn")),
279                };
280                stack[base + a as usize] = result;
281            }
282            Op::Sub => {
283                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
284                    (VmValue::Int(x), VmValue::Int(y)) => x
285                        .checked_sub(*y)
286                        .map(VmValue::Int)
287                        .unwrap_or_else(|| VmValue::Float(*x as f64 - *y as f64)),
288                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x - y),
289                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 - y),
290                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x - *y as f64),
291                    _ => return Err(runtime_err("Cannot subtract in parallel fn")),
292                };
293                stack[base + a as usize] = result;
294            }
295            Op::Mul => {
296                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
297                    (VmValue::Int(x), VmValue::Int(y)) => x
298                        .checked_mul(*y)
299                        .map(VmValue::Int)
300                        .unwrap_or_else(|| VmValue::Float(*x as f64 * *y as f64)),
301                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x * y),
302                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 * y),
303                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x * *y as f64),
304                    _ => return Err(runtime_err("Cannot multiply in parallel fn")),
305                };
306                stack[base + a as usize] = result;
307            }
308            Op::Div => {
309                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
310                    (VmValue::Int(x), VmValue::Int(y)) => {
311                        if *y == 0 {
312                            return Err(runtime_err("Division by zero"));
313                        }
314                        VmValue::Int(x / y)
315                    }
316                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x / y),
317                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 / y),
318                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x / *y as f64),
319                    _ => return Err(runtime_err("Cannot divide in parallel fn")),
320                };
321                stack[base + a as usize] = result;
322            }
323            Op::Mod => {
324                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
325                    (VmValue::Int(x), VmValue::Int(y)) => {
326                        if *y == 0 {
327                            return Err(runtime_err("Modulo by zero"));
328                        }
329                        VmValue::Int(x % y)
330                    }
331                    (VmValue::Float(x), VmValue::Float(y)) => {
332                        if *y == 0.0 {
333                            return Err(runtime_err("Modulo by zero"));
334                        }
335                        VmValue::Float(x % y)
336                    }
337                    _ => return Err(runtime_err("Cannot modulo in parallel fn")),
338                };
339                stack[base + a as usize] = result;
340            }
341            Op::Pow => {
342                let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
343                    (VmValue::Int(x), VmValue::Int(y)) => {
344                        VmValue::Int((*x as f64).powi(*y as i32) as i64)
345                    }
346                    (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x.powf(*y)),
347                    (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float((*x as f64).powf(*y)),
348                    (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x.powi(*y as i32)),
349                    _ => return Err(runtime_err("Cannot pow in parallel fn")),
350                };
351                stack[base + a as usize] = result;
352            }
353            Op::Neg => {
354                let result = match &stack[base + b as usize] {
355                    VmValue::Int(n) => VmValue::Int(-n),
356                    VmValue::Float(n) => VmValue::Float(-n),
357                    _ => return Err(runtime_err("Cannot negate in parallel fn")),
358                };
359                stack[base + a as usize] = result;
360            }
361            Op::Eq => {
362                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
363                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
364                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
365                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
366                    (VmValue::String(x), VmValue::String(y)) => x == y,
367                    (VmValue::None, VmValue::None) => true,
368                    _ => false,
369                };
370                stack[base + a as usize] = VmValue::Bool(eq);
371            }
372            Op::Neq => {
373                let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
374                    (VmValue::Int(x), VmValue::Int(y)) => x == y,
375                    (VmValue::Float(x), VmValue::Float(y)) => x == y,
376                    (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
377                    (VmValue::String(x), VmValue::String(y)) => x == y,
378                    (VmValue::None, VmValue::None) => true,
379                    _ => false,
380                };
381                stack[base + a as usize] = VmValue::Bool(!eq);
382            }
383            Op::Lt | Op::Gt | Op::Lte | Op::Gte => {
384                let cmp = match (&stack[base + b as usize], &stack[base + c as usize]) {
385                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y) as i8,
386                    (VmValue::Float(x), VmValue::Float(y)) => {
387                        if x < y {
388                            -1
389                        } else if x > y {
390                            1
391                        } else {
392                            0
393                        }
394                    }
395                    _ => return Err(runtime_err("Cannot compare in parallel fn")),
396                };
397                let result = match op {
398                    Op::Lt => cmp < 0,
399                    Op::Gt => cmp > 0,
400                    Op::Lte => cmp <= 0,
401                    Op::Gte => cmp >= 0,
402                    _ => unreachable!(),
403                };
404                stack[base + a as usize] = VmValue::Bool(result);
405            }
406            Op::And => {
407                let left = stack[base + b as usize].is_truthy();
408                let right = stack[base + c as usize].is_truthy();
409                stack[base + a as usize] = VmValue::Bool(left && right);
410            }
411            Op::Or => {
412                let left = stack[base + b as usize].is_truthy();
413                let right = stack[base + c as usize].is_truthy();
414                stack[base + a as usize] = VmValue::Bool(left || right);
415            }
416            Op::Not => {
417                let val = !stack[base + b as usize].is_truthy();
418                stack[base + a as usize] = VmValue::Bool(val);
419            }
420            Op::Jump => {
421                ip = (ip as i32 + sbx as i32) as usize;
422            }
423            Op::JumpIfFalse => {
424                if !stack[base + a as usize].is_truthy() {
425                    ip = (ip as i32 + sbx as i32) as usize;
426                }
427            }
428            Op::JumpIfTrue => {
429                if stack[base + a as usize].is_truthy() {
430                    ip = (ip as i32 + sbx as i32) as usize;
431                }
432            }
433            Op::Return => {
434                return Ok(stack[base + a as usize].clone());
435            }
436            // Unsupported ops in parallel context — fall back silently
437            _ => return Err(runtime_err("Unsupported op in parallel function")),
438        }
439    }
440}
441
442/// A call frame on the VM stack.
443struct CallFrame {
444    prototype: Arc<Prototype>,
445    ip: usize,
446    base: usize,
447    upvalues: Vec<UpvalueRef>,
448}
449
450/// A try-catch handler on the handler stack.
451struct TryHandler {
452    /// Frame index where try was entered
453    frame_idx: usize,
454    /// IP to jump to (catch handler)
455    catch_ip: usize,
456}
457
458/// The bytecode virtual machine.
459pub struct Vm {
460    /// Register stack
461    pub stack: Vec<VmValue>,
462    /// Call frame stack
463    frames: Vec<CallFrame>,
464    /// Global variables
465    pub globals: HashMap<String, VmValue>,
466    /// Data engine (lazily initialized)
467    #[cfg(feature = "native")]
468    data_engine: Option<DataEngine>,
469    /// Captured output (for testing)
470    pub output: Vec<String>,
471    /// Try-catch handler stack
472    try_handlers: Vec<TryHandler>,
473    /// Yielded value (Some when Op::Yield suspends a generator)
474    yielded_value: Option<VmValue>,
475    /// IP at the point of yield (instruction after the Yield op)
476    yielded_ip: usize,
477    /// Current file path (for relative imports)
478    pub file_path: Option<String>,
479    /// Module cache: resolved path → exports
480    module_cache: HashMap<String, HashMap<String, VmValue>>,
481    /// Files currently being imported (circular detection)
482    importing_files: std::collections::HashSet<String>,
483    /// Tracks which globals are public (for module export filtering)
484    pub public_items: std::collections::HashSet<String>,
485    /// Package roots: package_name → source directory
486    pub package_roots: HashMap<String, std::path::PathBuf>,
487    /// Project root (where tl.toml lives)
488    pub project_root: Option<std::path::PathBuf>,
489    /// Schema registry for versioned schemas
490    pub schema_registry: crate::schema::SchemaRegistry,
491    /// Secret vault for credential management (zeroed on drop)
492    pub secret_vault: SecretVault,
493    /// Security policy (optional, set via --sandbox)
494    pub security_policy: Option<crate::security::SecurityPolicy>,
495    /// Tokio runtime for async builtins (lazily initialized)
496    #[cfg(feature = "async-runtime")]
497    runtime: Option<Arc<tokio::runtime::Runtime>>,
498    /// Stashed thrown value for structured error preservation in try/catch
499    thrown_value: Option<VmValue>,
500    /// GPU operations dispatcher (lazily initialized)
501    #[cfg(feature = "gpu")]
502    gpu_ops: Option<tl_gpu::GpuOps>,
503    /// MCP clients associated with agents (agent_name -> clients)
504    #[cfg(feature = "mcp")]
505    mcp_agent_clients: HashMap<String, Vec<Arc<tl_mcp::McpClient>>>,
506}
507
508/// A secret vault that zeros entries on drop to reduce credential exposure in memory.
509#[derive(Debug, Clone, Default)]
510pub struct SecretVault(HashMap<String, String>);
511
512impl SecretVault {
513    pub fn new() -> Self {
514        Self(HashMap::new())
515    }
516    pub fn get(&self, key: &str) -> Option<&String> {
517        self.0.get(key)
518    }
519    pub fn insert(&mut self, key: String, val: String) {
520        self.0.insert(key, val);
521    }
522    pub fn remove(&mut self, key: &str) {
523        self.0.remove(key);
524    }
525    pub fn keys(&self) -> impl Iterator<Item = &String> {
526        self.0.keys()
527    }
528}
529
530impl Drop for SecretVault {
531    fn drop(&mut self) {
532        for val in self.0.values_mut() {
533            // Overwrite the string's buffer with zeros before deallocation.
534            // SAFETY: we write zeros into the valid allocated range of the String.
535            unsafe {
536                let ptr = val.as_mut_vec().as_mut_ptr();
537                std::ptr::write_bytes(ptr, 0, val.len());
538            }
539        }
540        self.0.clear();
541    }
542}
543
544impl Vm {
545    pub fn new() -> Self {
546        let mut vm = Vm {
547            stack: Vec::with_capacity(256),
548            frames: Vec::new(),
549            globals: HashMap::new(),
550            #[cfg(feature = "native")]
551            data_engine: None,
552            output: Vec::new(),
553            try_handlers: Vec::new(),
554            yielded_value: None,
555            yielded_ip: 0,
556            file_path: None,
557            module_cache: HashMap::new(),
558            importing_files: std::collections::HashSet::new(),
559            public_items: std::collections::HashSet::new(),
560            package_roots: HashMap::new(),
561            project_root: None,
562            schema_registry: crate::schema::SchemaRegistry::new(),
563            secret_vault: SecretVault::new(),
564            security_policy: None,
565            #[cfg(feature = "async-runtime")]
566            runtime: None,
567            thrown_value: None,
568            #[cfg(feature = "gpu")]
569            gpu_ops: None,
570            #[cfg(feature = "mcp")]
571            mcp_agent_clients: HashMap::new(),
572        };
573        // Phase 27: Register built-in error enum definitions
574        vm.globals.insert(
575            "DataError".into(),
576            VmValue::EnumDef(Arc::new(VmEnumDef {
577                name: Arc::from("DataError"),
578                variants: vec![
579                    (Arc::from("ParseError"), 2),
580                    (Arc::from("SchemaError"), 3),
581                    (Arc::from("ValidationError"), 2),
582                    (Arc::from("NotFound"), 1),
583                ],
584            })),
585        );
586        vm.globals.insert(
587            "NetworkError".into(),
588            VmValue::EnumDef(Arc::new(VmEnumDef {
589                name: Arc::from("NetworkError"),
590                variants: vec![
591                    (Arc::from("ConnectionError"), 2),
592                    (Arc::from("TimeoutError"), 1),
593                    (Arc::from("HttpError"), 2),
594                ],
595            })),
596        );
597        vm.globals.insert(
598            "ConnectorError".into(),
599            VmValue::EnumDef(Arc::new(VmEnumDef {
600                name: Arc::from("ConnectorError"),
601                variants: vec![
602                    (Arc::from("AuthError"), 2),
603                    (Arc::from("QueryError"), 2),
604                    (Arc::from("ConfigError"), 2),
605                ],
606            })),
607        );
608        // Phase 3: Register MCP builtins as globals
609        #[cfg(feature = "mcp")]
610        {
611            vm.globals.insert(
612                "mcp_connect".to_string(),
613                VmValue::Builtin(BuiltinId::McpConnect),
614            );
615            vm.globals.insert(
616                "mcp_list_tools".to_string(),
617                VmValue::Builtin(BuiltinId::McpListTools),
618            );
619            vm.globals.insert(
620                "mcp_call_tool".to_string(),
621                VmValue::Builtin(BuiltinId::McpCallTool),
622            );
623            vm.globals.insert(
624                "mcp_disconnect".to_string(),
625                VmValue::Builtin(BuiltinId::McpDisconnect),
626            );
627            vm.globals.insert(
628                "mcp_serve".to_string(),
629                VmValue::Builtin(BuiltinId::McpServe),
630            );
631            vm.globals.insert(
632                "mcp_server_info".to_string(),
633                VmValue::Builtin(BuiltinId::McpServerInfo),
634            );
635            vm.globals
636                .insert("mcp_ping".to_string(), VmValue::Builtin(BuiltinId::McpPing));
637            vm.globals.insert(
638                "mcp_list_resources".to_string(),
639                VmValue::Builtin(BuiltinId::McpListResources),
640            );
641            vm.globals.insert(
642                "mcp_read_resource".to_string(),
643                VmValue::Builtin(BuiltinId::McpReadResource),
644            );
645            vm.globals.insert(
646                "mcp_list_prompts".to_string(),
647                VmValue::Builtin(BuiltinId::McpListPrompts),
648            );
649            vm.globals.insert(
650                "mcp_get_prompt".to_string(),
651                VmValue::Builtin(BuiltinId::McpGetPrompt),
652            );
653        }
654        vm
655    }
656
657    /// Lazily initialize and return the tokio runtime.
658    #[cfg(feature = "async-runtime")]
659    fn ensure_runtime(&mut self) -> Arc<tokio::runtime::Runtime> {
660        if self.runtime.is_none() {
661            self.runtime = Some(Arc::new(
662                tokio::runtime::Builder::new_multi_thread()
663                    .enable_all()
664                    .build()
665                    .expect("Failed to create tokio runtime"),
666            ));
667        }
668        self.runtime.as_ref().unwrap().clone()
669    }
670
671    /// Lazily initialize and return the GPU ops dispatcher.
672    #[cfg(feature = "gpu")]
673    fn get_gpu_ops(&mut self) -> Result<&tl_gpu::GpuOps, TlError> {
674        if self.gpu_ops.is_none() {
675            let device =
676                tl_gpu::GpuDevice::get().ok_or_else(|| runtime_err("No GPU device available"))?;
677            self.gpu_ops = Some(tl_gpu::GpuOps::new(device));
678        }
679        Ok(self.gpu_ops.as_ref().unwrap())
680    }
681
682    /// Extract a GpuTensor from a VmValue, auto-uploading CPU tensors if needed.
683    #[cfg(feature = "gpu")]
684    fn ensure_gpu_tensor(&mut self, val: &VmValue) -> Result<Arc<tl_gpu::GpuTensor>, TlError> {
685        match val {
686            VmValue::GpuTensor(gt) => Ok(gt.clone()),
687            #[cfg(feature = "native")]
688            VmValue::Tensor(t) => {
689                let device = tl_gpu::GpuDevice::get()
690                    .ok_or_else(|| runtime_err("No GPU device available"))?;
691                Ok(Arc::new(tl_gpu::GpuTensor::from_cpu(t, device)))
692            }
693            _ => Err(runtime_err(format!(
694                "Expected tensor or gpu_tensor, got {}",
695                val.type_name()
696            ))),
697        }
698    }
699
700    #[cfg(feature = "native")]
701    fn engine(&mut self) -> &DataEngine {
702        if self.data_engine.is_none() {
703            self.data_engine = Some(DataEngine::new());
704        }
705        self.data_engine.as_ref().unwrap()
706    }
707
708    /// Ensure the stack has at least `size` slots.
709    fn ensure_stack(&mut self, size: usize) {
710        if self.stack.len() < size {
711            self.stack.resize(size, VmValue::None);
712        }
713    }
714
715    /// Execute a compiled prototype.
716    pub fn execute(&mut self, proto: &Prototype) -> Result<VmValue, TlError> {
717        let proto = Arc::new(proto.clone());
718        let base = self.stack.len();
719        self.ensure_stack(base + proto.num_registers as usize + 1);
720
721        self.frames.push(CallFrame {
722            prototype: proto,
723            ip: 0,
724            base,
725            upvalues: Vec::new(),
726        });
727
728        self.run().map_err(|e| self.enrich_error(e))
729    }
730
731    // -- Debug API (Phase H5) --
732
733    /// Prepare the VM for debug execution by pushing a call frame without running.
734    pub fn debug_load(&mut self, proto: &Prototype) {
735        let proto = Arc::new(proto.clone());
736        let base = self.stack.len();
737        self.ensure_stack(base + proto.num_registers as usize + 1);
738        self.frames.push(CallFrame {
739            prototype: proto,
740            ip: 0,
741            base,
742            upvalues: Vec::new(),
743        });
744    }
745
746    /// Execute a single instruction in debug mode. Returns:
747    /// - Ok(None) → instruction executed, more to go
748    /// - Ok(Some(val)) → execution completed with return value
749    /// - Err → runtime error
750    pub fn debug_step(&mut self) -> Result<Option<VmValue>, TlError> {
751        let entry_depth = 1; // Always run at top level depth
752        self.run_step(entry_depth).map_err(|e| self.enrich_error(e))
753    }
754
755    /// Get the current source line number (1-based) or 0 if unknown.
756    pub fn debug_current_line(&self) -> u32 {
757        if let Some(frame) = self.frames.last() {
758            let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
759            if ip < frame.prototype.lines.len() {
760                frame.prototype.lines[ip]
761            } else {
762                0
763            }
764        } else {
765            0
766        }
767    }
768
769    /// Get the current function name being executed.
770    pub fn debug_current_function(&self) -> String {
771        self.frames
772            .last()
773            .map(|f| f.prototype.name.clone())
774            .unwrap_or_default()
775    }
776
777    /// Check if the VM has finished executing (no more frames).
778    pub fn debug_is_done(&self) -> bool {
779        self.frames.is_empty()
780            || self
781                .frames
782                .last()
783                .is_some_and(|f| f.ip >= f.prototype.code.len())
784    }
785
786    /// Get a global variable by name.
787    pub fn debug_get_global(&self, name: &str) -> Option<&VmValue> {
788        self.globals.get(name)
789    }
790
791    /// Get a local variable by name (looks in top_level_locals of current frame).
792    pub fn debug_get_local(&self, name: &str) -> Option<&VmValue> {
793        if let Some(frame) = self.frames.last() {
794            for (local_name, reg) in &frame.prototype.top_level_locals {
795                if local_name == name {
796                    let idx = frame.base + *reg as usize;
797                    if idx < self.stack.len() {
798                        return Some(&self.stack[idx]);
799                    }
800                }
801            }
802        }
803        None
804    }
805
806    /// Get all local variables in the current frame.
807    pub fn debug_locals(&self) -> Vec<(String, &VmValue)> {
808        let mut result = Vec::new();
809        if let Some(frame) = self.frames.last() {
810            for (name, reg) in &frame.prototype.top_level_locals {
811                let idx = frame.base + *reg as usize;
812                if idx < self.stack.len() {
813                    result.push((name.clone(), &self.stack[idx]));
814                }
815            }
816        }
817        result
818    }
819
820    /// Get the current IP (instruction pointer).
821    pub fn debug_current_ip(&self) -> usize {
822        self.frames.last().map(|f| f.ip).unwrap_or(0)
823    }
824
825    /// Run until the next source line changes (step over).
826    pub fn debug_step_line(&mut self) -> Result<Option<VmValue>, TlError> {
827        let start_line = self.debug_current_line();
828        loop {
829            if self.debug_is_done() {
830                return Ok(Some(VmValue::None));
831            }
832            let result = self.debug_step()?;
833            if result.is_some() {
834                return Ok(result);
835            }
836            let new_line = self.debug_current_line();
837            if new_line != start_line && new_line != 0 {
838                return Ok(None);
839            }
840        }
841    }
842
843    /// Continue execution until a breakpoint line is hit or execution completes.
844    pub fn debug_continue(&mut self, breakpoints: &[u32]) -> Result<Option<VmValue>, TlError> {
845        loop {
846            if self.debug_is_done() {
847                return Ok(Some(VmValue::None));
848            }
849            let result = self.debug_step()?;
850            if result.is_some() {
851                return Ok(result);
852            }
853            let line = self.debug_current_line();
854            if breakpoints.contains(&line) {
855                return Ok(None);
856            }
857        }
858    }
859
860    /// Enrich a runtime error with line number and stack trace from the current call frames.
861    fn enrich_error(&self, err: TlError) -> TlError {
862        match err {
863            TlError::Runtime(mut re) => {
864                // Build stack trace from remaining frames
865                let mut trace = Vec::new();
866                for frame in self.frames.iter().rev() {
867                    let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
868                    let line = if ip < frame.prototype.lines.len() {
869                        frame.prototype.lines[ip]
870                    } else {
871                        0
872                    };
873                    trace.push(tl_errors::StackFrame {
874                        function: frame.prototype.name.clone(),
875                        line,
876                    });
877                }
878                // Set span from the innermost frame's line if not already set
879                if re.span.is_none() && !trace.is_empty() && trace[0].line > 0 {
880                    // We only have line number, not byte offset, so we can't set a precise span.
881                    // But we can set a line-based marker that report_runtime_error can use.
882                    // For now, leave span as None and rely on the stack trace.
883                }
884                re.stack_trace = trace;
885                TlError::Runtime(re)
886            }
887            other => other,
888        }
889    }
890
891    /// Main dispatch loop. Runs the current (topmost) frame until Return.
892    fn run(&mut self) -> Result<VmValue, TlError> {
893        let entry_depth = self.frames.len();
894        loop {
895            let step_result = self.run_step(entry_depth);
896            match step_result {
897                Ok(Some(val)) => return Ok(val), // Return instruction
898                Ok(None) => continue,            // Normal instruction
899                Err(e) => {
900                    // Check for try handler
901                    if let Some(handler) = self.try_handlers.pop() {
902                        // Restore to handler's frame
903                        while self.frames.len() > handler.frame_idx {
904                            self.frames.pop();
905                        }
906                        if self.frames.is_empty() {
907                            return Err(e);
908                        }
909                        let fidx = self.frames.len() - 1;
910                        self.frames[fidx].ip = handler.catch_ip;
911                        let err_msg = match &e {
912                            TlError::Runtime(re) => re.message.clone(),
913                            other => format!("{other}"),
914                        };
915                        // Put error value in catch scope's first local
916                        // The compiler emits LoadNone for the catch var at catch_ip; we need to
917                        // identify the register, set the error value, and skip past the LoadNone
918                        let catch_val = self
919                            .thrown_value
920                            .take()
921                            .unwrap_or_else(|| VmValue::String(Arc::from(err_msg.as_str())));
922                        let cbase = self.frames[fidx].base;
923                        let current_ip = self.frames[fidx].ip;
924                        if current_ip < self.frames[fidx].prototype.code.len() {
925                            let catch_inst = self.frames[fidx].prototype.code[current_ip];
926                            let catch_op = decode_op(catch_inst);
927                            let catch_reg = decode_a(catch_inst);
928                            if matches!(catch_op, Op::LoadNone) {
929                                // Skip the LoadNone and write error value directly
930                                self.frames[fidx].ip += 1;
931                                self.ensure_stack(cbase + catch_reg as usize + 1);
932                                self.stack[cbase + catch_reg as usize] = catch_val;
933                            }
934                        }
935                        continue;
936                    }
937                    return Err(e);
938                }
939            }
940        }
941    }
942
943    /// Execute a single instruction. Returns Ok(Some(val)) for Return, Ok(None) for continue, Err for errors.
944    fn run_step(&mut self, entry_depth: usize) -> Result<Option<VmValue>, TlError> {
945        if self.frames.len() < entry_depth || self.frames.is_empty() {
946            return Ok(Some(VmValue::None));
947        }
948        let frame_idx = self.frames.len() - 1;
949        let frame = &self.frames[frame_idx];
950
951        if frame.ip >= frame.prototype.code.len() {
952            // End of bytecode — return None
953            self.frames.pop();
954            return Ok(Some(VmValue::None));
955        }
956
957        let inst = frame.prototype.code[frame.ip];
958        let op = decode_op(inst);
959        let a = decode_a(inst);
960        let b = decode_b(inst);
961        let c = decode_c(inst);
962        let bx = decode_bx(inst);
963        let sbx = decode_sbx(inst);
964        let base = frame.base;
965
966        // Advance IP before executing (some ops modify it)
967        self.frames[frame_idx].ip += 1;
968
969        match op {
970            Op::LoadConst => {
971                let val = self.load_constant(frame_idx, bx)?;
972                self.stack[base + a as usize] = val;
973            }
974            Op::LoadNone => {
975                self.stack[base + a as usize] = VmValue::None;
976            }
977            Op::LoadTrue => {
978                self.stack[base + a as usize] = VmValue::Bool(true);
979            }
980            Op::LoadFalse => {
981                self.stack[base + a as usize] = VmValue::Bool(false);
982            }
983            Op::Move => {
984                let val = &self.stack[base + b as usize];
985                if matches!(val, VmValue::Moved) {
986                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
987                }
988                self.stack[base + a as usize] = val.clone();
989            }
990            Op::GetLocal => {
991                let val = &self.stack[base + b as usize];
992                if matches!(val, VmValue::Moved) {
993                    return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
994                }
995                self.stack[base + a as usize] = val.clone();
996            }
997            Op::SetLocal => {
998                let val = self.stack[base + a as usize].clone();
999                self.stack[base + b as usize] = val;
1000            }
1001            Op::GetGlobal => {
1002                let name = self.get_string_constant(frame_idx, bx)?;
1003                let val = self
1004                    .globals
1005                    .get(name.as_ref())
1006                    .cloned()
1007                    .unwrap_or(VmValue::None);
1008                if matches!(val, VmValue::Moved) {
1009                    return Err(runtime_err(format!(
1010                        "Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."
1011                    )));
1012                }
1013                self.stack[base + a as usize] = val;
1014            }
1015            Op::SetGlobal => {
1016                let name = self.get_string_constant(frame_idx, bx)?;
1017                let val = self.stack[base + a as usize].clone();
1018                // Phase 21: Detect __schema__ and __migrate__ globals and register in schema_registry
1019                #[cfg(feature = "native")]
1020                if let VmValue::String(ref s) = val {
1021                    if s.starts_with("__schema__:") {
1022                        self.process_schema_global(s);
1023                    } else if s.starts_with("__migrate__:") {
1024                        self.process_migrate_global(s);
1025                    }
1026                }
1027                self.globals.insert(name.to_string(), val);
1028            }
1029            Op::GetUpvalue => {
1030                let val = {
1031                    let frame = &self.frames[frame_idx];
1032                    match &frame.upvalues[b as usize] {
1033                        UpvalueRef::Open { stack_index } => self.stack[*stack_index].clone(),
1034                        UpvalueRef::Closed(v) => v.clone(),
1035                    }
1036                };
1037                self.stack[base + a as usize] = val;
1038            }
1039            Op::SetUpvalue => {
1040                let val = self.stack[base + a as usize].clone();
1041                let frame = &mut self.frames[frame_idx];
1042                match &mut frame.upvalues[b as usize] {
1043                    UpvalueRef::Open { stack_index } => {
1044                        let idx = *stack_index;
1045                        self.stack[idx] = val;
1046                    }
1047                    UpvalueRef::Closed(v) => {
1048                        *v = val;
1049                    }
1050                }
1051            }
1052            Op::Add => {
1053                let result = self.vm_add(base, b, c)?;
1054                self.stack[base + a as usize] = result;
1055            }
1056            Op::Sub => {
1057                let result = self.vm_sub(base, b, c)?;
1058                self.stack[base + a as usize] = result;
1059            }
1060            Op::Mul => {
1061                let result = self.vm_mul(base, b, c)?;
1062                self.stack[base + a as usize] = result;
1063            }
1064            Op::Div => {
1065                let result = self.vm_div(base, b, c)?;
1066                self.stack[base + a as usize] = result;
1067            }
1068            Op::Mod => {
1069                let result = self.vm_mod(base, b, c)?;
1070                self.stack[base + a as usize] = result;
1071            }
1072            Op::Pow => {
1073                let result = self.vm_pow(base, b, c)?;
1074                self.stack[base + a as usize] = result;
1075            }
1076            Op::Neg => {
1077                let result = match &self.stack[base + b as usize] {
1078                    VmValue::Int(n) => VmValue::Int(-n),
1079                    VmValue::Float(n) => VmValue::Float(-n),
1080                    VmValue::Decimal(d) => VmValue::Decimal(-d),
1081                    other => {
1082                        return Err(runtime_err(format!("Cannot negate {}", other.type_name())));
1083                    }
1084                };
1085                self.stack[base + a as usize] = result;
1086            }
1087            Op::Eq => {
1088                let result = self.vm_eq(base, b, c);
1089                self.stack[base + a as usize] = VmValue::Bool(result);
1090            }
1091            Op::Neq => {
1092                let result = !self.vm_eq(base, b, c);
1093                self.stack[base + a as usize] = VmValue::Bool(result);
1094            }
1095            Op::Lt => {
1096                let result = self.vm_cmp(base, b, c)?;
1097                self.stack[base + a as usize] = VmValue::Bool(result == Some(-1));
1098            }
1099            Op::Gt => {
1100                let result = self.vm_cmp(base, b, c)?;
1101                self.stack[base + a as usize] = VmValue::Bool(result == Some(1));
1102            }
1103            Op::Lte => {
1104                let result = self.vm_cmp(base, b, c)?;
1105                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(-1) | Some(0)));
1106            }
1107            Op::Gte => {
1108                let result = self.vm_cmp(base, b, c)?;
1109                self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(0) | Some(1)));
1110            }
1111            Op::And => {
1112                let left = self.stack[base + b as usize].is_truthy();
1113                let right = self.stack[base + c as usize].is_truthy();
1114                self.stack[base + a as usize] = VmValue::Bool(left && right);
1115            }
1116            Op::Or => {
1117                let left = self.stack[base + b as usize].is_truthy();
1118                let right = self.stack[base + c as usize].is_truthy();
1119                self.stack[base + a as usize] = VmValue::Bool(left || right);
1120            }
1121            Op::Not => {
1122                let val = !self.stack[base + b as usize].is_truthy();
1123                self.stack[base + a as usize] = VmValue::Bool(val);
1124            }
1125            Op::Concat => {
1126                let left = format!("{}", self.stack[base + b as usize]);
1127                let right = format!("{}", self.stack[base + c as usize]);
1128                self.stack[base + a as usize] =
1129                    VmValue::String(Arc::from(format!("{left}{right}").as_str()));
1130            }
1131            Op::Jump => {
1132                let frame = &mut self.frames[frame_idx];
1133                frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1134            }
1135            Op::JumpIfFalse => {
1136                if !self.stack[base + a as usize].is_truthy() {
1137                    let frame = &mut self.frames[frame_idx];
1138                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1139                }
1140            }
1141            Op::JumpIfTrue => {
1142                if self.stack[base + a as usize].is_truthy() {
1143                    let frame = &mut self.frames[frame_idx];
1144                    frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1145                }
1146            }
1147            Op::Call => {
1148                // a = func reg, b = args start, c = arg count
1149                let func_val = self.stack[base + a as usize].clone();
1150                self.do_call(func_val, base, a, b, c)?;
1151            }
1152            Op::Return => {
1153                let return_val = self.stack[base + a as usize].clone();
1154                self.frames.pop();
1155                return Ok(Some(return_val));
1156            }
1157            Op::Closure => {
1158                let proto = match &self.frames[frame_idx].prototype.constants[bx as usize] {
1159                    Constant::Prototype(p) => p.clone(),
1160                    _ => return Err(runtime_err("Expected prototype constant")),
1161                };
1162
1163                // Capture upvalues
1164                let mut upvalues = Vec::new();
1165                for def in &proto.upvalue_defs {
1166                    if def.is_local {
1167                        upvalues.push(UpvalueRef::Open {
1168                            stack_index: base + def.index as usize,
1169                        });
1170                    } else {
1171                        let frame = &self.frames[frame_idx];
1172                        upvalues.push(frame.upvalues[def.index as usize].clone());
1173                    }
1174                }
1175
1176                let closure = VmClosure {
1177                    prototype: proto,
1178                    upvalues,
1179                };
1180                self.stack[base + a as usize] = VmValue::Function(Arc::new(closure));
1181            }
1182            Op::NewList => {
1183                // a = dest, b = start reg, c = count
1184                let mut items = Vec::with_capacity(c as usize);
1185                for i in 0..c as usize {
1186                    items.push(self.stack[base + b as usize + i].clone());
1187                }
1188                self.stack[base + a as usize] = VmValue::List(Box::new(items));
1189            }
1190            Op::GetIndex => {
1191                let raw_obj = &self.stack[base + b as usize];
1192                let obj = match raw_obj {
1193                    VmValue::Ref(inner) => inner.as_ref(),
1194                    other => other,
1195                };
1196                let idx = &self.stack[base + c as usize];
1197                let result = match (obj, idx) {
1198                    (VmValue::List(items), VmValue::Int(i)) => {
1199                        let idx = if *i < 0 {
1200                            let adjusted = items.len() as i64 + *i;
1201                            if adjusted < 0 {
1202                                return Err(runtime_err(format!(
1203                                    "Index {} out of bounds for list of length {}",
1204                                    i,
1205                                    items.len()
1206                                )));
1207                            }
1208                            adjusted as usize
1209                        } else {
1210                            *i as usize
1211                        };
1212                        items.get(idx).cloned().ok_or_else(|| {
1213                            runtime_err(format!(
1214                                "Index {} out of bounds for list of length {}",
1215                                i,
1216                                items.len()
1217                            ))
1218                        })?
1219                    }
1220                    (VmValue::Map(pairs), VmValue::String(key)) => pairs
1221                        .iter()
1222                        .find(|(k, _)| k.as_ref() == key.as_ref())
1223                        .map(|(_, v)| v.clone())
1224                        .unwrap_or(VmValue::None),
1225                    _ => {
1226                        return Err(runtime_err(format!(
1227                            "Cannot index {} with {}",
1228                            obj.type_name(),
1229                            idx.type_name()
1230                        )));
1231                    }
1232                };
1233                self.stack[base + a as usize] = result;
1234            }
1235            Op::SetIndex => {
1236                if matches!(&self.stack[base + b as usize], VmValue::Ref(_)) {
1237                    return Err(runtime_err(
1238                        "Cannot mutate a borrowed reference".to_string(),
1239                    ));
1240                }
1241                let val = self.stack[base + a as usize].clone();
1242                let idx_val = self.stack[base + c as usize].clone();
1243                match idx_val {
1244                    VmValue::Int(i) => {
1245                        if let VmValue::List(ref mut items) = self.stack[base + b as usize] {
1246                            let idx = if i < 0 {
1247                                let adjusted = items.len() as i64 + i;
1248                                if adjusted < 0 {
1249                                    return Err(runtime_err(format!(
1250                                        "Index {} out of bounds for list of length {}",
1251                                        i,
1252                                        items.len()
1253                                    )));
1254                                }
1255                                adjusted as usize
1256                            } else {
1257                                i as usize
1258                            };
1259                            if idx < items.len() {
1260                                items[idx] = val;
1261                            } else {
1262                                return Err(runtime_err(format!(
1263                                    "Index {} out of bounds for list of length {}",
1264                                    i,
1265                                    items.len()
1266                                )));
1267                            }
1268                        }
1269                    }
1270                    VmValue::String(key) => {
1271                        if let VmValue::Map(ref mut pairs) = self.stack[base + b as usize] {
1272                            if let Some(entry) =
1273                                pairs.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref())
1274                            {
1275                                entry.1 = val;
1276                            } else {
1277                                pairs.push((key, val));
1278                            }
1279                        }
1280                    }
1281                    _ => {}
1282                }
1283            }
1284            Op::NewMap => {
1285                // a = dest, b = start reg, c = pair count
1286                // The pairs are key, value, key, value in registers b..b+c*2
1287                let mut pairs = Vec::with_capacity(c as usize);
1288                for i in 0..c as usize {
1289                    let key_val = &self.stack[base + b as usize + i * 2];
1290                    let val = self.stack[base + b as usize + i * 2 + 1].clone();
1291                    let key = match key_val {
1292                        VmValue::String(s) => s.clone(),
1293                        other => Arc::from(format!("{other}").as_str()),
1294                    };
1295                    pairs.push((key, val));
1296                }
1297                self.stack[base + a as usize] = VmValue::Map(Box::new(pairs));
1298            }
1299            Op::TablePipe => {
1300                #[cfg(feature = "native")]
1301                {
1302                    // a = table reg, b = op name constant idx, c = args constant idx
1303                    let table_val = self.stack[base + a as usize].clone();
1304                    let result = self.handle_table_pipe(frame_idx, table_val, b, c)?;
1305                    self.stack[base + a as usize] = result;
1306                }
1307                #[cfg(not(feature = "native"))]
1308                {
1309                    let _ = (a, b, c, frame_idx);
1310                    return Err(runtime_err("Table operations not available in WASM"));
1311                }
1312            }
1313            Op::CallBuiltin => {
1314                // ABx format: a = dest, bx = builtin id (16-bit)
1315                // Next instruction: A = arg count, B = first arg reg
1316                let builtin_id = decode_bx(inst);
1317                let next_inst = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1318                self.frames[frame_idx].ip += 1;
1319                let arg_count = decode_a(next_inst) as usize;
1320                let first_arg = decode_b(next_inst) as usize;
1321
1322                let result = self.call_builtin(builtin_id, base + first_arg, arg_count)?;
1323                self.stack[base + a as usize] = result;
1324            }
1325            Op::ForIter => {
1326                // a = iterator (index) reg, b = list reg, c = value dest reg
1327                let idx = match &self.stack[base + a as usize] {
1328                    VmValue::Int(i) => *i as usize,
1329                    _ => 0,
1330                };
1331                let list = &self.stack[base + b as usize];
1332                let done = match list {
1333                    VmValue::List(items) => {
1334                        if idx < items.len() {
1335                            let item = items[idx].clone();
1336                            self.stack[base + c as usize] = item;
1337                            self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1338                            false
1339                        } else {
1340                            true
1341                        }
1342                    }
1343                    VmValue::Map(pairs) => {
1344                        if idx < pairs.len() {
1345                            let (k, v) = &pairs[idx];
1346                            let pair = VmValue::List(Box::new(vec![
1347                                VmValue::String(k.clone()),
1348                                v.clone(),
1349                            ]));
1350                            self.stack[base + c as usize] = pair;
1351                            self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1352                            false
1353                        } else {
1354                            true
1355                        }
1356                    }
1357                    VmValue::Set(items) => {
1358                        if idx < items.len() {
1359                            let item = items[idx].clone();
1360                            self.stack[base + c as usize] = item;
1361                            self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1362                            false
1363                        } else {
1364                            true
1365                        }
1366                    }
1367                    VmValue::Generator(gen_arc) => {
1368                        let g = gen_arc.clone();
1369                        let val = self.generator_next(&g)?;
1370                        if matches!(val, VmValue::None) {
1371                            true
1372                        } else {
1373                            self.stack[base + c as usize] = val;
1374                            false
1375                        }
1376                    }
1377                    _ => true,
1378                };
1379                if done {
1380                    // Next instruction is a Jump — execute it
1381                    // (the jump instruction follows ForIter)
1382                } else {
1383                    // Skip the jump instruction
1384                    self.frames[frame_idx].ip += 1;
1385                }
1386            }
1387            Op::ForPrep => {
1388                // Not currently used — ForIter handles everything
1389            }
1390            Op::TestMatch => {
1391                // a = subject reg, b = pattern reg, c = dest bool reg
1392                let subject = &self.stack[base + a as usize];
1393                let pattern = &self.stack[base + b as usize];
1394                let matched = match (subject, pattern) {
1395                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
1396                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
1397                    (VmValue::String(a), VmValue::String(b)) => a == b,
1398                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1399                    (VmValue::None, VmValue::None) => true,
1400                    // Enum instance matching: same type + same variant
1401                    (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1402                        subj.type_name == pat.type_name && subj.variant == pat.variant
1403                    }
1404                    // Struct instance matching by type name
1405                    (VmValue::StructInstance(s), VmValue::String(name)) => {
1406                        s.type_name.as_ref() == name.as_ref()
1407                    }
1408                    _ => false,
1409                };
1410                self.stack[base + c as usize] = VmValue::Bool(matched);
1411            }
1412            Op::NullCoalesce => {
1413                if matches!(self.stack[base + a as usize], VmValue::None) {
1414                    let val = self.stack[base + b as usize].clone();
1415                    self.stack[base + a as usize] = val;
1416                }
1417            }
1418            Op::GetMember => {
1419                // a = dest, b = object reg, c = field name constant
1420                let field_name = self.get_string_constant(frame_idx, c as u16)?;
1421                let raw_obj = self.stack[base + b as usize].clone();
1422                let obj = match &raw_obj {
1423                    VmValue::Ref(inner) => inner.as_ref().clone(),
1424                    _ => raw_obj,
1425                };
1426                let result = match &obj {
1427                    VmValue::StructInstance(inst) => inst
1428                        .fields
1429                        .iter()
1430                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1431                        .map(|(_, v)| v.clone())
1432                        .unwrap_or(VmValue::None),
1433                    VmValue::Module(m) => m
1434                        .exports
1435                        .get(field_name.as_ref())
1436                        .cloned()
1437                        .unwrap_or(VmValue::None),
1438                    VmValue::EnumInstance(e) => match field_name.as_ref() {
1439                        "variant" => VmValue::String(e.variant.clone()),
1440                        "type_name" => VmValue::String(e.type_name.clone()),
1441                        _ => VmValue::None,
1442                    },
1443                    VmValue::Map(pairs) => pairs
1444                        .iter()
1445                        .find(|(k, _)| k.as_ref() == field_name.as_ref())
1446                        .map(|(_, v)| v.clone())
1447                        .unwrap_or(VmValue::None),
1448                    #[cfg(feature = "python")]
1449                    VmValue::PyObject(wrapper) => {
1450                        crate::python::py_get_member(wrapper, field_name.as_ref())
1451                    }
1452                    _ => VmValue::None,
1453                };
1454                self.stack[base + a as usize] = result;
1455            }
1456            Op::Interpolate => {
1457                // a = dest, bx = string template constant
1458                let template = self.get_string_constant(frame_idx, bx)?;
1459                let result = self.interpolate_string(&template, base)?;
1460                self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1461            }
1462            Op::Train => {
1463                #[cfg(feature = "native")]
1464                {
1465                    let result = self.handle_train(frame_idx, b, c)?;
1466                    self.stack[base + a as usize] = result;
1467                }
1468                #[cfg(not(feature = "native"))]
1469                {
1470                    let _ = (a, b, c, frame_idx);
1471                    return Err(runtime_err("AI training not available in WASM"));
1472                }
1473            }
1474            Op::PipelineExec => {
1475                #[cfg(feature = "native")]
1476                {
1477                    let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1478                    self.stack[base + a as usize] = result;
1479                }
1480                #[cfg(not(feature = "native"))]
1481                {
1482                    let _ = (a, b, c, frame_idx);
1483                    return Err(runtime_err("Pipelines not available in WASM"));
1484                }
1485            }
1486            Op::StreamExec => {
1487                #[cfg(feature = "native")]
1488                {
1489                    let result = self.handle_stream_exec(frame_idx, b)?;
1490                    self.stack[base + a as usize] = result;
1491                }
1492                #[cfg(not(feature = "native"))]
1493                {
1494                    let _ = (a, b, frame_idx);
1495                    return Err(runtime_err("Streaming not available in WASM"));
1496                }
1497            }
1498            Op::ConnectorDecl => {
1499                #[cfg(feature = "native")]
1500                {
1501                    let result = self.handle_connector_decl(frame_idx, b, c)?;
1502                    self.stack[base + a as usize] = result;
1503                }
1504                #[cfg(not(feature = "native"))]
1505                {
1506                    let _ = (a, b, c, frame_idx);
1507                    return Err(runtime_err("Connectors not available in WASM"));
1508                }
1509            }
1510
1511            // ── Phase 5: Language completeness opcodes ──
1512            Op::NewStruct => {
1513                // Two uses:
1514                // 1) Struct declaration: a=dest, b=name_const, c=fields_const (AstExprList)
1515                //    Next instruction is NOT a Move with start reg
1516                // 2) Struct instance: a=dest, b=name_const, c=field_count
1517                //    Next instruction is Move with start reg in A
1518
1519                let name = self.get_string_constant(frame_idx, b as u16)?;
1520
1521                // High bit of c distinguishes declaration (set) from instance (clear).
1522                // Declarations: c = constant_idx | 0x80
1523                // Instances: c = field_count (no high bit)
1524                let is_decl = (c & 0x80) != 0;
1525
1526                if is_decl {
1527                    let const_idx = (c & 0x7F) as usize;
1528                    // Struct/Enum declaration
1529                    let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1530                        Constant::AstExprList(exprs) => exprs.clone(),
1531                        _ => Vec::new(),
1532                    };
1533                    // Check if it looks like an enum (fields have "Name:count" format)
1534                    let is_enum = fields_data
1535                        .first()
1536                        .map(|e| {
1537                            if let AstExpr::String(s) = e {
1538                                s.contains(':')
1539                            } else {
1540                                false
1541                            }
1542                        })
1543                        .unwrap_or(false);
1544
1545                    if is_enum {
1546                        let variants: Vec<(Arc<str>, usize)> = fields_data
1547                            .iter()
1548                            .filter_map(|e| {
1549                                if let AstExpr::String(s) = e {
1550                                    let parts: Vec<&str> = s.splitn(2, ':').collect();
1551                                    if parts.len() == 2 {
1552                                        Some((
1553                                            Arc::from(parts[0]),
1554                                            parts[1].parse::<usize>().unwrap_or(0),
1555                                        ))
1556                                    } else {
1557                                        None
1558                                    }
1559                                } else {
1560                                    None
1561                                }
1562                            })
1563                            .collect();
1564                        self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1565                            name: name.clone(),
1566                            variants,
1567                        }));
1568                    } else {
1569                        let field_names: Vec<Arc<str>> = fields_data
1570                            .iter()
1571                            .filter_map(|e| {
1572                                if let AstExpr::String(s) = e {
1573                                    Some(Arc::from(s.as_str()))
1574                                } else {
1575                                    None
1576                                }
1577                            })
1578                            .collect();
1579                        self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1580                            name: name.clone(),
1581                            fields: field_names,
1582                        }));
1583                    }
1584                } else {
1585                    // Struct instance creation: c = field count
1586                    let field_count = c as usize;
1587                    // Next instruction holds start register in A field
1588                    let next_ip = self.frames[frame_idx].ip;
1589                    let next = self.frames[frame_idx]
1590                        .prototype
1591                        .code
1592                        .get(next_ip)
1593                        .copied()
1594                        .unwrap_or(0);
1595                    let start_reg = decode_a(next) as usize;
1596                    self.frames[frame_idx].ip += 1; // skip the extra instruction
1597
1598                    let mut fields = Vec::new();
1599                    for i in 0..field_count {
1600                        let fname = self.stack[base + start_reg + i * 2].clone();
1601                        let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1602                        let fname_str = match fname {
1603                            VmValue::String(s) => s,
1604                            _ => Arc::from(format!("field_{i}").as_str()),
1605                        };
1606                        fields.push((fname_str, fval));
1607                    }
1608                    self.stack[base + a as usize] =
1609                        VmValue::StructInstance(Arc::new(VmStructInstance {
1610                            type_name: name.clone(),
1611                            fields,
1612                        }));
1613                }
1614            }
1615
1616            Op::SetMember => {
1617                if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1618                    return Err(runtime_err(
1619                        "Cannot mutate a borrowed reference".to_string(),
1620                    ));
1621                }
1622                // a = object reg, b = field name constant, c = value reg
1623                let field_name = self.get_string_constant(frame_idx, b as u16)?;
1624                let val = self.stack[base + c as usize].clone();
1625                let obj = self.stack[base + a as usize].clone();
1626                if let VmValue::StructInstance(inst) = obj {
1627                    let mut new_fields = inst.fields.clone();
1628                    let mut found = false;
1629                    for (k, v) in &mut new_fields {
1630                        if k.as_ref() == field_name.as_ref() {
1631                            *v = val.clone();
1632                            found = true;
1633                            break;
1634                        }
1635                    }
1636                    if !found {
1637                        new_fields.push((field_name, val));
1638                    }
1639                    self.stack[base + a as usize] =
1640                        VmValue::StructInstance(Arc::new(VmStructInstance {
1641                            type_name: inst.type_name.clone(),
1642                            fields: new_fields,
1643                        }));
1644                }
1645            }
1646
1647            Op::NewEnum => {
1648                // a = dest, b = name constant ("EnumName::Variant"), c = args start reg
1649                // Next instruction: arg_count in A field
1650                let full_name = self.get_string_constant(frame_idx, b as u16)?;
1651                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1652                self.frames[frame_idx].ip += 1;
1653                let arg_count = decode_a(next) as usize;
1654                let args_start = c as usize;
1655
1656                // Parse "EnumName::Variant"
1657                let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1658                let (type_name, variant) = if parts.len() == 2 {
1659                    (Arc::from(parts[0]), Arc::from(parts[1]))
1660                } else {
1661                    (Arc::from(""), Arc::from(full_name.as_ref()))
1662                };
1663
1664                let mut fields = Vec::new();
1665                for i in 0..arg_count {
1666                    fields.push(self.stack[base + args_start + i].clone());
1667                }
1668
1669                self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1670                    type_name,
1671                    variant,
1672                    fields,
1673                }));
1674            }
1675
1676            Op::MatchEnum => {
1677                // a = subject reg, b = variant name constant, c = dest bool reg
1678                let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1679                let subject = &self.stack[base + a as usize];
1680                let matched = match subject {
1681                    VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1682                    _ => false,
1683                };
1684                self.stack[base + c as usize] = VmValue::Bool(matched);
1685            }
1686
1687            Op::MethodCall => {
1688                // a = dest, b = object reg, c = method name constant
1689                // Next instruction: A = args_start, B = arg_count
1690                let method_name = self.get_string_constant(frame_idx, c as u16)?;
1691                let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1692                self.frames[frame_idx].ip += 1;
1693                let args_start = decode_a(next) as usize;
1694                let arg_count = decode_b(next) as usize;
1695
1696                let obj = self.stack[base + b as usize].clone();
1697                let mut args = Vec::new();
1698                for i in 0..arg_count {
1699                    args.push(self.stack[base + args_start + i].clone());
1700                }
1701
1702                let result = self.dispatch_method(obj, &method_name, &args)?;
1703                self.stack[base + a as usize] = result;
1704            }
1705
1706            Op::Throw => {
1707                // a = value register
1708                let val = self.stack[base + a as usize].clone();
1709                self.thrown_value = Some(val.clone());
1710                let err_msg = format!("{val}");
1711                return Err(runtime_err(err_msg));
1712            }
1713
1714            Op::TryBegin => {
1715                // sbx = offset to catch handler (relative to this instruction)
1716                let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1717                self.try_handlers.push(TryHandler {
1718                    frame_idx: self.frames.len(),
1719                    catch_ip,
1720                });
1721            }
1722
1723            Op::TryEnd => {
1724                // Pop the try handler (success path)
1725                self.try_handlers.pop();
1726            }
1727
1728            Op::Import => {
1729                #[cfg(feature = "native")]
1730                {
1731                    // a = dest, bx = path constant
1732                    // Next instruction encodes either:
1733                    //   - Classic import: A = alias constant, B = 0, C = 0
1734                    //   - Use import: A = extra, B = kind, C = 0xAB (magic marker)
1735                    let path = self.get_string_constant(frame_idx, bx)?;
1736                    let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1737                    self.frames[frame_idx].ip += 1;
1738                    let next_a = decode_a(next);
1739                    let next_b = decode_b(next);
1740                    let next_c = decode_c(next);
1741
1742                    let result = if next_c == 0xAB {
1743                        // Use-style import (dot-path)
1744                        self.handle_use_import(&path, next_a, next_b, frame_idx)?
1745                    } else {
1746                        // Classic import "file.tl" [as alias]
1747                        let alias_idx = next_a as u16;
1748                        let alias = self.get_string_constant(frame_idx, alias_idx)?;
1749                        self.handle_import(&path, &alias)?
1750                    };
1751                    self.stack[base + a as usize] = result;
1752                }
1753                #[cfg(not(feature = "native"))]
1754                {
1755                    let _ = (a, bx, frame_idx);
1756                    return Err(runtime_err("Module imports not available in WASM"));
1757                }
1758            }
1759
1760            Op::Await => {
1761                // a = dest, b = task/value register
1762                let val = self.stack[base + b as usize].clone();
1763                match val {
1764                    VmValue::Task(task) => {
1765                        let rx = {
1766                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1767                            guard.take()
1768                        };
1769                        match rx {
1770                            Some(receiver) => match receiver.recv() {
1771                                Ok(Ok(result)) => {
1772                                    self.stack[base + a as usize] = result;
1773                                }
1774                                Ok(Err(err_msg)) => {
1775                                    return Err(runtime_err(err_msg));
1776                                }
1777                                Err(_) => {
1778                                    return Err(runtime_err("Task channel disconnected"));
1779                                }
1780                            },
1781                            None => {
1782                                return Err(runtime_err("Task already awaited"));
1783                            }
1784                        }
1785                    }
1786                    // Non-task values pass through
1787                    other => {
1788                        self.stack[base + a as usize] = other;
1789                    }
1790                }
1791            }
1792            Op::Yield => {
1793                // a = value register to yield
1794                let val = self.stack[base + a as usize].clone();
1795                self.yielded_value = Some(val.clone());
1796                // Save the current ip (already advanced past Yield instruction)
1797                self.yielded_ip = self.frames[frame_idx].ip;
1798                // Pop the frame and return the value
1799                self.frames.pop();
1800                return Ok(Some(val));
1801            }
1802            Op::TryPropagate => {
1803                // A = dest, B = source register
1804                // If source is Err(...) → early return from current function
1805                // If source is Ok(v) → A = v (unwrap)
1806                // If source is None → early return None
1807                // Otherwise → passthrough
1808                let src = self.stack[base + b as usize].clone();
1809                match &src {
1810                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1811                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1812                            // Unwrap: A = inner value
1813                            self.stack[base + a as usize] = ei.fields[0].clone();
1814                        } else if ei.variant.as_ref() == "Err" {
1815                            // Propagate: return the Err from current function
1816                            self.frames.pop();
1817                            return Ok(Some(src));
1818                        } else {
1819                            self.stack[base + a as usize] = src;
1820                        }
1821                    }
1822                    VmValue::None => {
1823                        // Propagate: return None from current function
1824                        self.frames.pop();
1825                        return Ok(Some(VmValue::None));
1826                    }
1827                    _ => {
1828                        // Passthrough
1829                        self.stack[base + a as usize] = src;
1830                    }
1831                }
1832            }
1833            Op::ExtractField => {
1834                // A = dest, B = source reg, C = field index
1835                // If C has high bit set (C | 0x80), extract rest (sublist from index C & 0x7F)
1836                let source = self.stack[base + b as usize].clone();
1837                let is_rest = (c & 0x80) != 0;
1838                let idx = (c & 0x7F) as usize;
1839                let val = if is_rest {
1840                    // Extract rest as sublist from index idx..
1841                    match &source {
1842                        VmValue::List(l) => {
1843                            if idx < l.len() {
1844                                VmValue::List(Box::new(l[idx..].to_vec()))
1845                            } else {
1846                                VmValue::List(Box::default())
1847                            }
1848                        }
1849                        _ => VmValue::List(Box::default()),
1850                    }
1851                } else {
1852                    match &source {
1853                        VmValue::EnumInstance(ei) => {
1854                            ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1855                        }
1856                        VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1857                        _ => VmValue::None,
1858                    }
1859                };
1860                self.stack[base + a as usize] = val;
1861            }
1862            Op::ExtractNamedField => {
1863                // A = dest, B = source reg, C = field name constant index
1864                let source = self.stack[base + b as usize].clone();
1865                let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1866                    Constant::String(s) => s.clone(),
1867                    _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1868                };
1869                let val = match &source {
1870                    VmValue::StructInstance(s) => s
1871                        .fields
1872                        .iter()
1873                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1874                        .map(|(_, v)| v.clone())
1875                        .unwrap_or(VmValue::None),
1876                    VmValue::Map(m) => m
1877                        .iter()
1878                        .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1879                        .map(|(_, v)| v.clone())
1880                        .unwrap_or(VmValue::None),
1881                    _ => VmValue::None,
1882                };
1883                self.stack[base + a as usize] = val;
1884            }
1885
1886            // Phase 28: Ownership & Move Semantics
1887            Op::LoadMoved => {
1888                self.stack[base + a as usize] = VmValue::Moved;
1889            }
1890            Op::MakeRef => {
1891                let val = self.stack[base + b as usize].clone();
1892                self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1893            }
1894            Op::ParallelFor => {
1895                // Currently compiled as regular ForIter, this opcode is reserved
1896                // for future rayon-backed parallel iteration.
1897            }
1898            Op::AgentExec => {
1899                #[cfg(feature = "native")]
1900                {
1901                    let result = self.handle_agent_exec(frame_idx, b, c)?;
1902                    self.stack[base + a as usize] = result;
1903                }
1904                #[cfg(not(feature = "native"))]
1905                {
1906                    let _ = (a, b, c, frame_idx);
1907                    return Err(runtime_err("Agents not available in WASM".to_string()));
1908                }
1909            }
1910        }
1911        Ok(None)
1912    }
1913
1914    /// Perform a function call.
1915    fn do_call(
1916        &mut self,
1917        func: VmValue,
1918        caller_base: usize,
1919        func_reg: u8,
1920        args_start: u8,
1921        arg_count: u8,
1922    ) -> Result<(), TlError> {
1923        const MAX_CALL_DEPTH: usize = 512;
1924        if self.frames.len() >= MAX_CALL_DEPTH {
1925            return Err(runtime_err(
1926                "Stack overflow: maximum recursion depth (512) exceeded",
1927            ));
1928        }
1929        match func {
1930            VmValue::Function(closure) => {
1931                let proto = closure.prototype.clone();
1932                let arity = proto.arity as usize;
1933
1934                if arg_count as usize != arity {
1935                    return Err(runtime_err(format!(
1936                        "Expected {} arguments, got {}",
1937                        arity, arg_count
1938                    )));
1939                }
1940
1941                // If this is a generator function, create a Generator instead of executing
1942                if proto.is_generator {
1943                    // Close upvalues for the generator
1944                    let mut closed_upvalues = Vec::new();
1945                    for uv in &closure.upvalues {
1946                        match uv {
1947                            UpvalueRef::Open { stack_index } => {
1948                                let val = self.stack[*stack_index].clone();
1949                                closed_upvalues.push(UpvalueRef::Closed(val));
1950                            }
1951                            UpvalueRef::Closed(v) => {
1952                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1953                            }
1954                        }
1955                    }
1956
1957                    // Build initial saved_stack with args
1958                    let num_regs = proto.num_registers as usize;
1959                    let mut saved_stack = vec![VmValue::None; num_regs];
1960                    for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1961                        *slot = self.stack[caller_base + args_start as usize + i].clone();
1962                    }
1963
1964                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
1965                        prototype: proto,
1966                        upvalues: closed_upvalues,
1967                        saved_stack,
1968                        ip: 0,
1969                    });
1970                    self.stack[caller_base + func_reg as usize] =
1971                        VmValue::Generator(Arc::new(Mutex::new(gn)));
1972                    return Ok(());
1973                }
1974
1975                // Set up new frame
1976                let new_base = self.stack.len();
1977                self.ensure_stack(new_base + proto.num_registers as usize + 1);
1978
1979                // Copy args to new frame's registers
1980                for i in 0..arg_count as usize {
1981                    self.stack[new_base + i] =
1982                        self.stack[caller_base + args_start as usize + i].clone();
1983                }
1984
1985                self.frames.push(CallFrame {
1986                    prototype: proto,
1987                    ip: 0,
1988                    base: new_base,
1989                    upvalues: closure.upvalues.clone(),
1990                });
1991
1992                // Run the function
1993                let result = self.run()?;
1994
1995                // Close any upvalues in the result that point into this frame's stack
1996                let result = self.close_upvalues_in_value(result, new_base);
1997
1998                // Store result in caller's func_reg
1999                self.stack[caller_base + func_reg as usize] = result;
2000
2001                // Shrink stack back
2002                self.stack.truncate(new_base);
2003
2004                Ok(())
2005            }
2006            VmValue::Builtin(builtin_id) => {
2007                let result = self.call_builtin(
2008                    builtin_id as u16,
2009                    caller_base + args_start as usize,
2010                    arg_count as usize,
2011                )?;
2012                self.stack[caller_base + func_reg as usize] = result;
2013                Ok(())
2014            }
2015            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2016        }
2017    }
2018
2019    /// Walk a VmValue and promote any Open upvalues pointing at or above `frame_base`
2020    /// to Closed. This is called on return values before the caller's stack is truncated,
2021    /// so that closures escaping their defining function retain correct captured values.
2022    /// Check if a value may contain functions with open upvalues (recursive).
2023    fn value_may_need_closing(val: &VmValue) -> bool {
2024        match val {
2025            VmValue::Function(_) => true,
2026            VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2027            VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2028            _ => false,
2029        }
2030    }
2031
2032    fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2033        match val {
2034            VmValue::Function(ref closure) => {
2035                // Check if any upvalue needs closing
2036                let needs_closing = closure.upvalues.iter().any(|uv| {
2037                    matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2038                });
2039                if !needs_closing {
2040                    return val;
2041                }
2042                let closed_upvalues: Vec<UpvalueRef> = closure
2043                    .upvalues
2044                    .iter()
2045                    .map(|uv| match uv {
2046                        UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2047                            UpvalueRef::Closed(self.stack[*stack_index].clone())
2048                        }
2049                        other => other.clone(),
2050                    })
2051                    .collect();
2052                VmValue::Function(Arc::new(VmClosure {
2053                    prototype: closure.prototype.clone(),
2054                    upvalues: closed_upvalues,
2055                }))
2056            }
2057            VmValue::List(items) => {
2058                if !items.iter().any(Self::value_may_need_closing) {
2059                    return VmValue::List(items);
2060                }
2061                VmValue::List(Box::new(
2062                    (*items)
2063                        .into_iter()
2064                        .map(|v| self.close_upvalues_in_value(v, frame_base))
2065                        .collect(),
2066                ))
2067            }
2068            VmValue::Map(entries) => {
2069                if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2070                    return VmValue::Map(entries);
2071                }
2072                VmValue::Map(Box::new(
2073                    (*entries)
2074                        .into_iter()
2075                        .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2076                        .collect(),
2077                ))
2078            }
2079            other => other,
2080        }
2081    }
2082
2083    /// Execute a closure (no arguments) in this VM. Used by spawn().
2084    pub(crate) fn execute_closure(
2085        &mut self,
2086        proto: &Arc<Prototype>,
2087        upvalues: &[UpvalueRef],
2088    ) -> Result<VmValue, TlError> {
2089        let base = self.stack.len();
2090        self.ensure_stack(base + proto.num_registers as usize + 1);
2091        self.frames.push(CallFrame {
2092            prototype: proto.clone(),
2093            ip: 0,
2094            base,
2095            upvalues: upvalues.to_vec(),
2096        });
2097        self.run()
2098    }
2099
2100    /// Execute a closure with arguments in this VM. Used by pmap().
2101    pub(crate) fn execute_closure_with_args(
2102        &mut self,
2103        proto: &Arc<Prototype>,
2104        upvalues: &[UpvalueRef],
2105        args: &[VmValue],
2106    ) -> Result<VmValue, TlError> {
2107        let base = self.stack.len();
2108        self.ensure_stack(base + proto.num_registers as usize + 1);
2109        for (i, arg) in args.iter().enumerate() {
2110            self.stack[base + i] = arg.clone();
2111        }
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    fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2122        let frame = &self.frames[frame_idx];
2123        match &frame.prototype.constants[idx as usize] {
2124            Constant::Int(n) => Ok(VmValue::Int(*n)),
2125            Constant::Float(f) => Ok(VmValue::Float(*f)),
2126            Constant::String(s) => Ok(VmValue::String(s.clone())),
2127            Constant::Prototype(p) => {
2128                // Return as a closure with no upvalues
2129                Ok(VmValue::Function(Arc::new(VmClosure {
2130                    prototype: p.clone(),
2131                    upvalues: Vec::new(),
2132                })))
2133            }
2134            Constant::Decimal(s) => {
2135                use std::str::FromStr;
2136                Ok(VmValue::Decimal(
2137                    rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2138                ))
2139            }
2140            Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2141        }
2142    }
2143
2144    fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2145        let frame = &self.frames[frame_idx];
2146        match &frame.prototype.constants[idx as usize] {
2147            Constant::String(s) => Ok(s.clone()),
2148            _ => Err(runtime_err("Expected string constant")),
2149        }
2150    }
2151
2152    // ── Arithmetic helpers ──
2153
2154    fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2155        let left = &self.stack[base + b as usize];
2156        let right = &self.stack[base + c as usize];
2157        match (left, right) {
2158            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2159                .checked_add(*b)
2160                .map(VmValue::Int)
2161                .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2162            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2163            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2164            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2165            (VmValue::String(a), VmValue::String(b)) => {
2166                Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2167            }
2168            #[cfg(feature = "gpu")]
2169            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2170                let a = a.clone();
2171                let b = b.clone();
2172                let ops = self.get_gpu_ops()?;
2173                let result = ops.add(&a, &b).map_err(runtime_err)?;
2174                Ok(VmValue::GpuTensor(Arc::new(result)))
2175            }
2176            #[cfg(feature = "gpu")]
2177            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2178            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2179                let lv = self.stack[base + b as usize].clone();
2180                let rv = self.stack[base + c as usize].clone();
2181                let a = self.ensure_gpu_tensor(&lv)?;
2182                let b_val = self.ensure_gpu_tensor(&rv)?;
2183                let ops = self.get_gpu_ops()?;
2184                let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2185                Ok(VmValue::GpuTensor(Arc::new(result)))
2186            }
2187            #[cfg(feature = "native")]
2188            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2189                let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2190                Ok(VmValue::Tensor(Arc::new(result)))
2191            }
2192            // Decimal arithmetic
2193            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2194            (VmValue::Decimal(a), VmValue::Int(b)) => {
2195                Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2196            }
2197            (VmValue::Int(a), VmValue::Decimal(b)) => {
2198                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2199            }
2200            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2201            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2202            _ => Err(runtime_err(format!(
2203                "Cannot apply `+` to {} and {}",
2204                left.type_name(),
2205                right.type_name()
2206            ))),
2207        }
2208    }
2209
2210    fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2211        let left = &self.stack[base + b as usize];
2212        let right = &self.stack[base + c as usize];
2213        match (left, right) {
2214            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2215                .checked_sub(*b)
2216                .map(VmValue::Int)
2217                .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2218            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2219            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2220            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2221            #[cfg(feature = "gpu")]
2222            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2223                let a = a.clone();
2224                let b = b.clone();
2225                let ops = self.get_gpu_ops()?;
2226                let result = ops.sub(&a, &b).map_err(runtime_err)?;
2227                Ok(VmValue::GpuTensor(Arc::new(result)))
2228            }
2229            #[cfg(feature = "gpu")]
2230            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2231            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2232                let lv = self.stack[base + b as usize].clone();
2233                let rv = self.stack[base + c as usize].clone();
2234                let a = self.ensure_gpu_tensor(&lv)?;
2235                let b_val = self.ensure_gpu_tensor(&rv)?;
2236                let ops = self.get_gpu_ops()?;
2237                let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2238                Ok(VmValue::GpuTensor(Arc::new(result)))
2239            }
2240            #[cfg(feature = "native")]
2241            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2242                let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2243                Ok(VmValue::Tensor(Arc::new(result)))
2244            }
2245            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2246            (VmValue::Decimal(a), VmValue::Int(b)) => {
2247                Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2248            }
2249            (VmValue::Int(a), VmValue::Decimal(b)) => {
2250                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2251            }
2252            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2253            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2254            _ => Err(runtime_err(format!(
2255                "Cannot apply `-` to {} and {}",
2256                left.type_name(),
2257                right.type_name()
2258            ))),
2259        }
2260    }
2261
2262    fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2263        let left = &self.stack[base + b as usize];
2264        let right = &self.stack[base + c as usize];
2265        match (left, right) {
2266            (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2267                .checked_mul(*b)
2268                .map(VmValue::Int)
2269                .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2270            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2271            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2272            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2273            (VmValue::String(a), VmValue::Int(b)) => {
2274                if *b < 0 {
2275                    return Err(runtime_err(
2276                        "Cannot repeat string a negative number of times",
2277                    ));
2278                }
2279                if *b > 10_000_000 {
2280                    return Err(runtime_err(
2281                        "String repeat count too large (max 10,000,000)",
2282                    ));
2283                }
2284                Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2285            }
2286            #[cfg(feature = "gpu")]
2287            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2288                let a = a.clone();
2289                let b = b.clone();
2290                let ops = self.get_gpu_ops()?;
2291                let result = ops.mul(&a, &b).map_err(runtime_err)?;
2292                Ok(VmValue::GpuTensor(Arc::new(result)))
2293            }
2294            #[cfg(feature = "gpu")]
2295            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2296            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2297                let lv = self.stack[base + b as usize].clone();
2298                let rv = self.stack[base + c as usize].clone();
2299                let a = self.ensure_gpu_tensor(&lv)?;
2300                let b_val = self.ensure_gpu_tensor(&rv)?;
2301                let ops = self.get_gpu_ops()?;
2302                let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2303                Ok(VmValue::GpuTensor(Arc::new(result)))
2304            }
2305            #[cfg(feature = "gpu")]
2306            (VmValue::GpuTensor(t), VmValue::Float(s))
2307            | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2308                let t = t.clone();
2309                let s = *s;
2310                let ops = self.get_gpu_ops()?;
2311                let result = ops.scale(&t, s as f32);
2312                Ok(VmValue::GpuTensor(Arc::new(result)))
2313            }
2314            #[cfg(feature = "native")]
2315            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2316                let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2317                Ok(VmValue::Tensor(Arc::new(result)))
2318            }
2319            #[cfg(feature = "native")]
2320            (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2321                let result = t.scale(*s);
2322                Ok(VmValue::Tensor(Arc::new(result)))
2323            }
2324            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2325            (VmValue::Decimal(a), VmValue::Int(b)) => {
2326                Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2327            }
2328            (VmValue::Int(a), VmValue::Decimal(b)) => {
2329                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2330            }
2331            (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2332            (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2333            _ => Err(runtime_err(format!(
2334                "Cannot apply `*` to {} and {}",
2335                left.type_name(),
2336                right.type_name()
2337            ))),
2338        }
2339    }
2340
2341    fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2342        let left = &self.stack[base + b as usize];
2343        let right = &self.stack[base + c as usize];
2344        match (left, right) {
2345            (VmValue::Int(a), VmValue::Int(b)) => {
2346                if *b == 0 {
2347                    return Err(runtime_err("Division by zero"));
2348                }
2349                Ok(VmValue::Int(a / b))
2350            }
2351            (VmValue::Float(a), VmValue::Float(b)) => {
2352                if *b == 0.0 {
2353                    return Err(runtime_err("Division by zero"));
2354                }
2355                Ok(VmValue::Float(a / b))
2356            }
2357            (VmValue::Int(a), VmValue::Float(b)) => {
2358                if *b == 0.0 {
2359                    return Err(runtime_err("Division by zero"));
2360                }
2361                Ok(VmValue::Float(*a as f64 / b))
2362            }
2363            (VmValue::Float(a), VmValue::Int(b)) => {
2364                if *b == 0 {
2365                    return Err(runtime_err("Division by zero"));
2366                }
2367                Ok(VmValue::Float(a / *b as f64))
2368            }
2369            #[cfg(feature = "gpu")]
2370            (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2371                let a = a.clone();
2372                let b = b.clone();
2373                let ops = self.get_gpu_ops()?;
2374                let result = ops.div(&a, &b).map_err(runtime_err)?;
2375                Ok(VmValue::GpuTensor(Arc::new(result)))
2376            }
2377            #[cfg(feature = "gpu")]
2378            (VmValue::GpuTensor(_), VmValue::Tensor(_))
2379            | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2380                let lv = self.stack[base + b as usize].clone();
2381                let rv = self.stack[base + c as usize].clone();
2382                let a = self.ensure_gpu_tensor(&lv)?;
2383                let b_val = self.ensure_gpu_tensor(&rv)?;
2384                let ops = self.get_gpu_ops()?;
2385                let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2386                Ok(VmValue::GpuTensor(Arc::new(result)))
2387            }
2388            #[cfg(feature = "native")]
2389            (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2390                let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2391                Ok(VmValue::Tensor(Arc::new(result)))
2392            }
2393            (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2394                if b.is_zero() {
2395                    return Err(runtime_err("Division by zero"));
2396                }
2397                Ok(VmValue::Decimal(a / b))
2398            }
2399            (VmValue::Decimal(a), VmValue::Int(b)) => {
2400                if *b == 0 {
2401                    return Err(runtime_err("Division by zero"));
2402                }
2403                Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2404            }
2405            (VmValue::Int(a), VmValue::Decimal(b)) => {
2406                if b.is_zero() {
2407                    return Err(runtime_err("Division by zero"));
2408                }
2409                Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2410            }
2411            (VmValue::Decimal(a), VmValue::Float(b)) => {
2412                if *b == 0.0 {
2413                    return Err(runtime_err("Division by zero"));
2414                }
2415                Ok(VmValue::Float(decimal_to_f64(a) / b))
2416            }
2417            (VmValue::Float(a), VmValue::Decimal(b)) => {
2418                if b.is_zero() {
2419                    return Err(runtime_err("Division by zero"));
2420                }
2421                Ok(VmValue::Float(a / decimal_to_f64(b)))
2422            }
2423            _ => Err(runtime_err(format!(
2424                "Cannot apply `/` to {} and {}",
2425                left.type_name(),
2426                right.type_name()
2427            ))),
2428        }
2429    }
2430
2431    fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2432        let left = &self.stack[base + b as usize];
2433        let right = &self.stack[base + c as usize];
2434        match (left, right) {
2435            (VmValue::Int(a), VmValue::Int(b)) => {
2436                if *b == 0 {
2437                    return Err(runtime_err("Modulo by zero"));
2438                }
2439                Ok(VmValue::Int(a % b))
2440            }
2441            (VmValue::Float(a), VmValue::Float(b)) => {
2442                if *b == 0.0 {
2443                    return Err(runtime_err("Modulo by zero"));
2444                }
2445                Ok(VmValue::Float(a % b))
2446            }
2447            (VmValue::Int(a), VmValue::Float(b)) => {
2448                if *b == 0.0 {
2449                    return Err(runtime_err("Modulo by zero"));
2450                }
2451                Ok(VmValue::Float(*a as f64 % b))
2452            }
2453            (VmValue::Float(a), VmValue::Int(b)) => {
2454                if *b == 0 {
2455                    return Err(runtime_err("Modulo by zero"));
2456                }
2457                Ok(VmValue::Float(a % *b as f64))
2458            }
2459            _ => Err(runtime_err(format!(
2460                "Cannot apply `%` to {} and {}",
2461                left.type_name(),
2462                right.type_name()
2463            ))),
2464        }
2465    }
2466
2467    fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2468        let left = &self.stack[base + b as usize];
2469        let right = &self.stack[base + c as usize];
2470        match (left, right) {
2471            (VmValue::Int(a), VmValue::Int(b)) => {
2472                if *b < 0 {
2473                    return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2474                }
2475                match a.checked_pow(*b as u32) {
2476                    Some(result) => Ok(VmValue::Int(result)),
2477                    None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2478                }
2479            }
2480            (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2481            (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2482            (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2483            _ => Err(runtime_err(format!(
2484                "Cannot apply `**` to {} and {}",
2485                left.type_name(),
2486                right.type_name()
2487            ))),
2488        }
2489    }
2490
2491    fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2492        self.stack[base + b as usize] == self.stack[base + c as usize]
2493    }
2494
2495    fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2496        let left = &self.stack[base + b as usize];
2497        let right = &self.stack[base + c as usize];
2498        match (left, right) {
2499            (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2500            (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2501            (VmValue::Int(a), VmValue::Float(b)) => {
2502                let fa = *a as f64;
2503                Ok(fa.partial_cmp(b).map(|o| o as i8))
2504            }
2505            (VmValue::Float(a), VmValue::Int(b)) => {
2506                let fb = *b as f64;
2507                Ok(a.partial_cmp(&fb).map(|o| o as i8))
2508            }
2509            (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2510            (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2511            (VmValue::Decimal(a), VmValue::Int(b)) => {
2512                Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2513            }
2514            (VmValue::Int(a), VmValue::Decimal(b)) => {
2515                Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2516            }
2517            (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2518            (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2519            (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2520            _ => Err(runtime_err(format!(
2521                "Cannot compare {} and {}",
2522                left.type_name(),
2523                right.type_name()
2524            ))),
2525        }
2526    }
2527
2528    // ── Security helpers ──
2529
2530    fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2531        if let Some(ref policy) = self.security_policy
2532            && !policy.check(perm)
2533        {
2534            return Err(runtime_err(format!("{perm} blocked by security policy")));
2535        }
2536        Ok(())
2537    }
2538
2539    // ── Builtin dispatch ──
2540
2541    pub fn call_builtin(
2542        &mut self,
2543        id: u16,
2544        args_base: usize,
2545        arg_count: usize,
2546    ) -> Result<VmValue, TlError> {
2547        let args: Vec<VmValue> = (0..arg_count)
2548            .map(|i| {
2549                let val = &self.stack[args_base + i];
2550                // Unwrap Ref transparently for builtin calls
2551                match val {
2552                    VmValue::Ref(inner) => inner.as_ref().clone(),
2553                    other => other.clone(),
2554                }
2555            })
2556            .collect();
2557
2558        let builtin_id: BuiltinId =
2559            BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2560
2561        match builtin_id {
2562            BuiltinId::Print | BuiltinId::Println => {
2563                let mut parts = Vec::new();
2564                for a in &args {
2565                    #[cfg(feature = "native")]
2566                    match a {
2567                        VmValue::Table(t) => {
2568                            let batches =
2569                                self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2570                            let formatted =
2571                                DataEngine::format_batches(&batches).map_err(runtime_err)?;
2572                            parts.push(formatted);
2573                        }
2574                        _ => parts.push(format!("{a}")),
2575                    }
2576                    #[cfg(not(feature = "native"))]
2577                    parts.push(format!("{a}"));
2578                }
2579                let line = parts.join(" ");
2580                println!("{line}");
2581                self.output.push(line);
2582                Ok(VmValue::None)
2583            }
2584            BuiltinId::Len => match args.first() {
2585                Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2586                Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2587                Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2588                Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2589                _ => Err(runtime_err("len() expects a string, list, map, or set")),
2590            },
2591            BuiltinId::Str => Ok(VmValue::String(Arc::from(
2592                args.first()
2593                    .map(|v| format!("{v}"))
2594                    .unwrap_or_default()
2595                    .as_str(),
2596            ))),
2597            BuiltinId::Int => match args.first() {
2598                Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2599                Some(VmValue::String(s)) => s
2600                    .parse::<i64>()
2601                    .map(VmValue::Int)
2602                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2603                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2604                Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2605                _ => Err(runtime_err("int() expects a number, string, or bool")),
2606            },
2607            BuiltinId::Float => match args.first() {
2608                Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2609                Some(VmValue::String(s)) => s
2610                    .parse::<f64>()
2611                    .map(VmValue::Float)
2612                    .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2613                Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2614                Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2615                _ => Err(runtime_err("float() expects a number, string, or bool")),
2616            },
2617            BuiltinId::Abs => match args.first() {
2618                Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2619                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2620                _ => Err(runtime_err("abs() expects a number")),
2621            },
2622            BuiltinId::Min => {
2623                if args.len() == 2 {
2624                    match (&args[0], &args[1]) {
2625                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2626                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2627                        _ => Err(runtime_err("min() expects two numbers")),
2628                    }
2629                } else {
2630                    Err(runtime_err("min() expects 2 arguments"))
2631                }
2632            }
2633            BuiltinId::Max => {
2634                if args.len() == 2 {
2635                    match (&args[0], &args[1]) {
2636                        (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2637                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2638                        _ => Err(runtime_err("max() expects two numbers")),
2639                    }
2640                } else {
2641                    Err(runtime_err("max() expects 2 arguments"))
2642                }
2643            }
2644            BuiltinId::Range => {
2645                if args.len() == 1 {
2646                    if let VmValue::Int(n) = &args[0] {
2647                        if *n > 10_000_000 {
2648                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2649                        }
2650                        if *n < 0 {
2651                            return Ok(VmValue::List(Box::default()));
2652                        }
2653                        Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2654                    } else {
2655                        Err(runtime_err("range() expects an integer"))
2656                    }
2657                } else if args.len() == 2 {
2658                    if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2659                        let size = (*end - *start).max(0);
2660                        if size > 10_000_000 {
2661                            return Err(runtime_err("range() size too large (max 10,000,000)"));
2662                        }
2663                        Ok(VmValue::List(Box::new(
2664                            (*start..*end).map(VmValue::Int).collect(),
2665                        )))
2666                    } else {
2667                        Err(runtime_err("range() expects integers"))
2668                    }
2669                } else if args.len() == 3 {
2670                    if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2671                        (&args[0], &args[1], &args[2])
2672                    {
2673                        if *step == 0 {
2674                            return Err(runtime_err("range() step cannot be zero"));
2675                        }
2676                        let mut result = Vec::new();
2677                        let mut i = *start;
2678                        if *step > 0 {
2679                            while i < *end {
2680                                result.push(VmValue::Int(i));
2681                                i += step;
2682                            }
2683                        } else {
2684                            while i > *end {
2685                                result.push(VmValue::Int(i));
2686                                i += step;
2687                            }
2688                        }
2689                        Ok(VmValue::List(Box::new(result)))
2690                    } else {
2691                        Err(runtime_err("range() expects integers"))
2692                    }
2693                } else {
2694                    Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2695                }
2696            }
2697            BuiltinId::Push => {
2698                if args.len() == 2 {
2699                    if let VmValue::List(mut items) = args[0].clone() {
2700                        items.push(args[1].clone());
2701                        Ok(VmValue::List(items))
2702                    } else {
2703                        Err(runtime_err("push() first arg must be a list"))
2704                    }
2705                } else {
2706                    Err(runtime_err("push() expects 2 arguments"))
2707                }
2708            }
2709            BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2710                args.first().map(|v| v.type_name()).unwrap_or("none"),
2711            ))),
2712            BuiltinId::Map => {
2713                if args.len() != 2 {
2714                    return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2715                }
2716                let items = match &args[0] {
2717                    VmValue::List(items) => (**items).clone(),
2718                    _ => return Err(runtime_err("map() first arg must be a list")),
2719                };
2720                let func = args[1].clone();
2721                // Parallel path for large lists with pure functions
2722                #[cfg(feature = "native")]
2723                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2724                    let proto = match &func {
2725                        VmValue::Function(c) => c.prototype.clone(),
2726                        _ => unreachable!(),
2727                    };
2728                    let result: Result<Vec<VmValue>, TlError> = items
2729                        .into_par_iter()
2730                        .map(|item| execute_pure_fn(&proto, &[item]))
2731                        .collect();
2732                    return Ok(VmValue::List(Box::new(result?)));
2733                }
2734                let mut result = Vec::new();
2735                for item in items {
2736                    let val = self.call_vm_function(&func, &[item])?;
2737                    result.push(val);
2738                }
2739                Ok(VmValue::List(Box::new(result)))
2740            }
2741            BuiltinId::Filter => {
2742                if args.len() != 2 {
2743                    return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2744                }
2745                let items = match &args[0] {
2746                    VmValue::List(items) => (**items).clone(),
2747                    _ => return Err(runtime_err("filter() first arg must be a list")),
2748                };
2749                let func = args[1].clone();
2750                // Parallel path for large lists with pure functions
2751                #[cfg(feature = "native")]
2752                if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2753                    let proto = match &func {
2754                        VmValue::Function(c) => c.prototype.clone(),
2755                        _ => unreachable!(),
2756                    };
2757                    let result: Result<Vec<VmValue>, TlError> = items
2758                        .into_par_iter()
2759                        .filter_map(|item| {
2760                            match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2761                                Ok(val) => {
2762                                    if val.is_truthy() {
2763                                        Some(Ok(item))
2764                                    } else {
2765                                        None
2766                                    }
2767                                }
2768                                Err(e) => Some(Err(e)),
2769                            }
2770                        })
2771                        .collect();
2772                    return Ok(VmValue::List(Box::new(result?)));
2773                }
2774                let mut result = Vec::new();
2775                for item in items {
2776                    let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2777                    if val.is_truthy() {
2778                        result.push(item);
2779                    }
2780                }
2781                Ok(VmValue::List(Box::new(result)))
2782            }
2783            BuiltinId::Reduce | BuiltinId::Fold => {
2784                if args.len() != 3 {
2785                    return Err(runtime_err(
2786                        "reduce()/fold() expects 3 arguments (list, init, fn)",
2787                    ));
2788                }
2789                let items = match &args[0] {
2790                    VmValue::List(items) => (**items).clone(),
2791                    _ => return Err(runtime_err("reduce() first arg must be a list")),
2792                };
2793                let mut acc = args[1].clone();
2794                let func = args[2].clone();
2795                for item in items {
2796                    acc = self.call_vm_function(&func, &[acc, item])?;
2797                }
2798                Ok(acc)
2799            }
2800            BuiltinId::Sum => {
2801                if args.len() != 1 {
2802                    return Err(runtime_err("sum() expects 1 argument (list)"));
2803                }
2804                let items = match &args[0] {
2805                    VmValue::List(items) => items,
2806                    _ => return Err(runtime_err("sum() expects a list")),
2807                };
2808                // Check if any floats are present
2809                let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2810                #[cfg(feature = "native")]
2811                if items.len() >= PARALLEL_THRESHOLD {
2812                    // Parallel sum for large lists
2813                    if has_float {
2814                        let total: f64 = items
2815                            .par_iter()
2816                            .map(|v| match v {
2817                                VmValue::Int(n) => *n as f64,
2818                                VmValue::Float(n) => *n,
2819                                _ => 0.0,
2820                            })
2821                            .sum();
2822                        return Ok(VmValue::Float(total));
2823                    } else {
2824                        let total: i64 = items
2825                            .par_iter()
2826                            .map(|v| match v {
2827                                VmValue::Int(n) => *n,
2828                                _ => 0,
2829                            })
2830                            .sum();
2831                        return Ok(VmValue::Int(total));
2832                    }
2833                }
2834                // Sequential path for smaller lists
2835                let mut total: i64 = 0;
2836                let mut is_float = false;
2837                let mut total_f: f64 = 0.0;
2838                for item in items.iter() {
2839                    match item {
2840                        VmValue::Int(n) => {
2841                            if is_float {
2842                                total_f += *n as f64;
2843                            } else {
2844                                total += n;
2845                            }
2846                        }
2847                        VmValue::Float(n) => {
2848                            if !is_float {
2849                                total_f = total as f64;
2850                                is_float = true;
2851                            }
2852                            total_f += n;
2853                        }
2854                        _ => return Err(runtime_err("sum() list must contain numbers")),
2855                    }
2856                }
2857                if is_float {
2858                    Ok(VmValue::Float(total_f))
2859                } else {
2860                    Ok(VmValue::Int(total))
2861                }
2862            }
2863            BuiltinId::Any => {
2864                if args.len() != 2 {
2865                    return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2866                }
2867                let items = match &args[0] {
2868                    VmValue::List(items) => (**items).clone(),
2869                    _ => return Err(runtime_err("any() first arg must be a list")),
2870                };
2871                let func = args[1].clone();
2872                for item in items {
2873                    let val = self.call_vm_function(&func, &[item])?;
2874                    if val.is_truthy() {
2875                        return Ok(VmValue::Bool(true));
2876                    }
2877                }
2878                Ok(VmValue::Bool(false))
2879            }
2880            BuiltinId::All => {
2881                if args.len() != 2 {
2882                    return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2883                }
2884                let items = match &args[0] {
2885                    VmValue::List(items) => (**items).clone(),
2886                    _ => return Err(runtime_err("all() first arg must be a list")),
2887                };
2888                let func = args[1].clone();
2889                for item in items {
2890                    let val = self.call_vm_function(&func, &[item])?;
2891                    if !val.is_truthy() {
2892                        return Ok(VmValue::Bool(false));
2893                    }
2894                }
2895                Ok(VmValue::Bool(true))
2896            }
2897            // ── Data engine builtins ──
2898            #[cfg(feature = "native")]
2899            BuiltinId::ReadCsv => {
2900                if args.len() != 1 {
2901                    return Err(runtime_err("read_csv() expects 1 argument (path)"));
2902                }
2903                let path = match &args[0] {
2904                    VmValue::String(s) => s.to_string(),
2905                    _ => return Err(runtime_err("read_csv() path must be a string")),
2906                };
2907                match self.engine().read_csv(&path) {
2908                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2909                    Err(e) => {
2910                        let msg = e.to_string();
2911                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2912                            type_name: Arc::from("DataError"),
2913                            variant: Arc::from("ParseError"),
2914                            fields: vec![
2915                                VmValue::String(Arc::from(msg.as_str())),
2916                                VmValue::String(Arc::from(path.as_str())),
2917                            ],
2918                        })));
2919                        Err(runtime_err(msg))
2920                    }
2921                }
2922            }
2923            #[cfg(feature = "native")]
2924            BuiltinId::ReadParquet => {
2925                if args.len() != 1 {
2926                    return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2927                }
2928                let path = match &args[0] {
2929                    VmValue::String(s) => s.to_string(),
2930                    _ => return Err(runtime_err("read_parquet() path must be a string")),
2931                };
2932                match self.engine().read_parquet(&path) {
2933                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
2934                    Err(e) => {
2935                        let msg = e.to_string();
2936                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2937                            type_name: Arc::from("DataError"),
2938                            variant: Arc::from("ParseError"),
2939                            fields: vec![
2940                                VmValue::String(Arc::from(msg.as_str())),
2941                                VmValue::String(Arc::from(path.as_str())),
2942                            ],
2943                        })));
2944                        Err(runtime_err(msg))
2945                    }
2946                }
2947            }
2948            #[cfg(feature = "native")]
2949            BuiltinId::WriteCsv => {
2950                if args.len() != 2 {
2951                    return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2952                }
2953                let df = match &args[0] {
2954                    VmValue::Table(t) => t.df.clone(),
2955                    _ => return Err(runtime_err("write_csv() first arg must be a table")),
2956                };
2957                let path = match &args[1] {
2958                    VmValue::String(s) => s.to_string(),
2959                    _ => return Err(runtime_err("write_csv() path must be a string")),
2960                };
2961                match self.engine().write_csv(df, &path) {
2962                    Ok(_) => Ok(VmValue::None),
2963                    Err(e) => {
2964                        let msg = e.to_string();
2965                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2966                            type_name: Arc::from("DataError"),
2967                            variant: Arc::from("ParseError"),
2968                            fields: vec![
2969                                VmValue::String(Arc::from(msg.as_str())),
2970                                VmValue::String(Arc::from(path.as_str())),
2971                            ],
2972                        })));
2973                        Err(runtime_err(msg))
2974                    }
2975                }
2976            }
2977            #[cfg(feature = "native")]
2978            BuiltinId::WriteParquet => {
2979                if args.len() != 2 {
2980                    return Err(runtime_err(
2981                        "write_parquet() expects 2 arguments (table, path)",
2982                    ));
2983                }
2984                let df = match &args[0] {
2985                    VmValue::Table(t) => t.df.clone(),
2986                    _ => return Err(runtime_err("write_parquet() first arg must be a table")),
2987                };
2988                let path = match &args[1] {
2989                    VmValue::String(s) => s.to_string(),
2990                    _ => return Err(runtime_err("write_parquet() path must be a string")),
2991                };
2992                match self.engine().write_parquet(df, &path) {
2993                    Ok(_) => Ok(VmValue::None),
2994                    Err(e) => {
2995                        let msg = e.to_string();
2996                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2997                            type_name: Arc::from("DataError"),
2998                            variant: Arc::from("ParseError"),
2999                            fields: vec![
3000                                VmValue::String(Arc::from(msg.as_str())),
3001                                VmValue::String(Arc::from(path.as_str())),
3002                            ],
3003                        })));
3004                        Err(runtime_err(msg))
3005                    }
3006                }
3007            }
3008            #[cfg(feature = "native")]
3009            BuiltinId::Collect => {
3010                if args.len() != 1 {
3011                    return Err(runtime_err("collect() expects 1 argument (table)"));
3012                }
3013                let df = match &args[0] {
3014                    VmValue::Table(t) => t.df.clone(),
3015                    _ => return Err(runtime_err("collect() expects a table")),
3016                };
3017                let batches = self.engine().collect(df).map_err(runtime_err)?;
3018                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3019                Ok(VmValue::String(Arc::from(formatted.as_str())))
3020            }
3021            #[cfg(feature = "native")]
3022            BuiltinId::Show => {
3023                let df = match args.first() {
3024                    Some(VmValue::Table(t)) => t.df.clone(),
3025                    _ => return Err(runtime_err("show() expects a table")),
3026                };
3027                let limit = match args.get(1) {
3028                    Some(VmValue::Int(n)) => *n as usize,
3029                    None => 20,
3030                    _ => return Err(runtime_err("show() second arg must be an int")),
3031                };
3032                let limited = df
3033                    .limit(0, Some(limit))
3034                    .map_err(|e| runtime_err(format!("{e}")))?;
3035                let batches = self.engine().collect(limited).map_err(runtime_err)?;
3036                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3037                println!("{formatted}");
3038                self.output.push(formatted);
3039                Ok(VmValue::None)
3040            }
3041            #[cfg(feature = "native")]
3042            BuiltinId::Describe => {
3043                if args.len() != 1 {
3044                    return Err(runtime_err("describe() expects 1 argument (table)"));
3045                }
3046                let df = match &args[0] {
3047                    VmValue::Table(t) => t.df.clone(),
3048                    _ => return Err(runtime_err("describe() expects a table")),
3049                };
3050                let schema = df.schema();
3051                let mut lines = Vec::new();
3052                lines.push("Columns:".to_string());
3053                for (qualifier, field) in schema.iter() {
3054                    let prefix = match qualifier {
3055                        Some(q) => format!("{q}."),
3056                        None => String::new(),
3057                    };
3058                    lines.push(format!(
3059                        "  {}{}: {}",
3060                        prefix,
3061                        field.name(),
3062                        field.data_type()
3063                    ));
3064                }
3065                let output = lines.join("\n");
3066                println!("{output}");
3067                self.output.push(output.clone());
3068                Ok(VmValue::String(Arc::from(output.as_str())))
3069            }
3070            #[cfg(feature = "native")]
3071            BuiltinId::Head => {
3072                if args.is_empty() {
3073                    return Err(runtime_err("head() expects at least 1 argument (table)"));
3074                }
3075                let df = match &args[0] {
3076                    VmValue::Table(t) => t.df.clone(),
3077                    _ => return Err(runtime_err("head() first arg must be a table")),
3078                };
3079                let n = match args.get(1) {
3080                    Some(VmValue::Int(n)) => *n as usize,
3081                    None => 10,
3082                    _ => return Err(runtime_err("head() second arg must be an int")),
3083                };
3084                let limited = df
3085                    .limit(0, Some(n))
3086                    .map_err(|e| runtime_err(format!("{e}")))?;
3087                Ok(VmValue::Table(VmTable { df: limited }))
3088            }
3089            #[cfg(feature = "native")]
3090            BuiltinId::Postgres => {
3091                if args.len() != 2 {
3092                    return Err(runtime_err(
3093                        "postgres() expects 2 arguments (conn_str, table_name)",
3094                    ));
3095                }
3096                let conn_str = match &args[0] {
3097                    VmValue::String(s) => s.to_string(),
3098                    _ => return Err(runtime_err("postgres() conn_str must be a string")),
3099                };
3100                let table_name = match &args[1] {
3101                    VmValue::String(s) => s.to_string(),
3102                    _ => return Err(runtime_err("postgres() table_name must be a string")),
3103                };
3104                let conn_str = resolve_tl_config_connection(&conn_str);
3105                match self.engine().read_postgres(&conn_str, &table_name) {
3106                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
3107                    Err(e) => {
3108                        let msg = e.to_string();
3109                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3110                            type_name: Arc::from("ConnectorError"),
3111                            variant: Arc::from("QueryError"),
3112                            fields: vec![
3113                                VmValue::String(Arc::from(msg.as_str())),
3114                                VmValue::String(Arc::from("postgres")),
3115                            ],
3116                        })));
3117                        Err(runtime_err(msg))
3118                    }
3119                }
3120            }
3121            #[cfg(feature = "native")]
3122            BuiltinId::PostgresQuery => {
3123                if args.len() != 2 {
3124                    return Err(runtime_err(
3125                        "postgres_query() expects 2 arguments (conn_str, query)",
3126                    ));
3127                }
3128                let conn_str = match &args[0] {
3129                    VmValue::String(s) => s.to_string(),
3130                    _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3131                };
3132                let query = match &args[1] {
3133                    VmValue::String(s) => s.to_string(),
3134                    _ => return Err(runtime_err("postgres_query() query must be a string")),
3135                };
3136                let conn_str = resolve_tl_config_connection(&conn_str);
3137                match self
3138                    .engine()
3139                    .query_postgres(&conn_str, &query, "__pg_query_result")
3140                {
3141                    Ok(df) => Ok(VmValue::Table(VmTable { df })),
3142                    Err(e) => {
3143                        let msg = e.to_string();
3144                        self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3145                            type_name: Arc::from("ConnectorError"),
3146                            variant: Arc::from("QueryError"),
3147                            fields: vec![
3148                                VmValue::String(Arc::from(msg.as_str())),
3149                                VmValue::String(Arc::from("postgres")),
3150                            ],
3151                        })));
3152                        Err(runtime_err(msg))
3153                    }
3154                }
3155            }
3156            BuiltinId::TlConfigResolve => {
3157                if args.len() != 1 {
3158                    return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3159                }
3160                let name = match &args[0] {
3161                    VmValue::String(s) => s.to_string(),
3162                    _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3163                };
3164                let resolved = resolve_tl_config_connection(&name);
3165                Ok(VmValue::String(Arc::from(resolved.as_str())))
3166            }
3167            #[cfg(not(feature = "native"))]
3168            BuiltinId::ReadCsv
3169            | BuiltinId::ReadParquet
3170            | BuiltinId::WriteCsv
3171            | BuiltinId::WriteParquet
3172            | BuiltinId::Collect
3173            | BuiltinId::Show
3174            | BuiltinId::Describe
3175            | BuiltinId::Head
3176            | BuiltinId::Postgres
3177            | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3178            // ── AI builtins ──
3179            #[cfg(feature = "native")]
3180            BuiltinId::Tensor => {
3181                if args.is_empty() {
3182                    return Err(runtime_err("tensor() expects at least 1 argument"));
3183                }
3184                let data = self.vmvalue_to_f64_list(&args[0])?;
3185                let shape = if args.len() > 1 {
3186                    self.vmvalue_to_usize_list(&args[1])?
3187                } else {
3188                    vec![data.len()]
3189                };
3190                let t = tl_ai::TlTensor::from_vec(data, &shape)
3191                    .map_err(|e| runtime_err(e.to_string()))?;
3192                Ok(VmValue::Tensor(Arc::new(t)))
3193            }
3194            #[cfg(feature = "native")]
3195            BuiltinId::TensorZeros => {
3196                if args.is_empty() {
3197                    return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3198                }
3199                let shape = self.vmvalue_to_usize_list(&args[0])?;
3200                let t = tl_ai::TlTensor::zeros(&shape);
3201                Ok(VmValue::Tensor(Arc::new(t)))
3202            }
3203            #[cfg(feature = "native")]
3204            BuiltinId::TensorOnes => {
3205                if args.is_empty() {
3206                    return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3207                }
3208                let shape = self.vmvalue_to_usize_list(&args[0])?;
3209                let t = tl_ai::TlTensor::ones(&shape);
3210                Ok(VmValue::Tensor(Arc::new(t)))
3211            }
3212            #[cfg(feature = "native")]
3213            BuiltinId::TensorShape => match args.first() {
3214                Some(VmValue::Tensor(t)) => {
3215                    let shape: Vec<VmValue> =
3216                        t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3217                    Ok(VmValue::List(Box::new(shape)))
3218                }
3219                _ => Err(runtime_err("tensor_shape() expects a tensor")),
3220            },
3221            #[cfg(feature = "native")]
3222            BuiltinId::TensorReshape => {
3223                if args.len() != 2 {
3224                    return Err(runtime_err(
3225                        "tensor_reshape() expects 2 arguments (tensor, shape)",
3226                    ));
3227                }
3228                let t = match &args[0] {
3229                    VmValue::Tensor(t) => (**t).clone(),
3230                    _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3231                };
3232                let shape = self.vmvalue_to_usize_list(&args[1])?;
3233                let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3234                Ok(VmValue::Tensor(Arc::new(reshaped)))
3235            }
3236            #[cfg(feature = "native")]
3237            BuiltinId::TensorTranspose => match args.first() {
3238                Some(VmValue::Tensor(t)) => {
3239                    let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3240                    Ok(VmValue::Tensor(Arc::new(transposed)))
3241                }
3242                _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3243            },
3244            #[cfg(feature = "native")]
3245            BuiltinId::TensorSum => match args.first() {
3246                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3247                _ => Err(runtime_err("tensor_sum() expects a tensor")),
3248            },
3249            #[cfg(feature = "native")]
3250            BuiltinId::TensorMean => match args.first() {
3251                Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3252                _ => Err(runtime_err("tensor_mean() expects a tensor")),
3253            },
3254            #[cfg(feature = "native")]
3255            BuiltinId::TensorDot => {
3256                if args.len() != 2 {
3257                    return Err(runtime_err("tensor_dot() expects 2 arguments"));
3258                }
3259                let a_t = match &args[0] {
3260                    VmValue::Tensor(t) => t,
3261                    _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3262                };
3263                let b_t = match &args[1] {
3264                    VmValue::Tensor(t) => t,
3265                    _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3266                };
3267                let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3268                Ok(VmValue::Tensor(Arc::new(result)))
3269            }
3270            #[cfg(feature = "native")]
3271            BuiltinId::Predict => {
3272                if args.len() < 2 {
3273                    return Err(runtime_err(
3274                        "predict() expects at least 2 arguments (model, input)",
3275                    ));
3276                }
3277                let model = match &args[0] {
3278                    VmValue::Model(m) => (**m).clone(),
3279                    _ => return Err(runtime_err("predict() first arg must be a model")),
3280                };
3281                let input = match &args[1] {
3282                    VmValue::Tensor(t) => (**t).clone(),
3283                    _ => return Err(runtime_err("predict() second arg must be a tensor")),
3284                };
3285                let result =
3286                    tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3287                Ok(VmValue::Tensor(Arc::new(result)))
3288            }
3289            #[cfg(feature = "native")]
3290            BuiltinId::Similarity => {
3291                if args.len() != 2 {
3292                    return Err(runtime_err("similarity() expects 2 arguments"));
3293                }
3294                let a_t = match &args[0] {
3295                    VmValue::Tensor(t) => t,
3296                    _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3297                };
3298                let b_t = match &args[1] {
3299                    VmValue::Tensor(t) => t,
3300                    _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3301                };
3302                let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3303                Ok(VmValue::Float(sim))
3304            }
3305            #[cfg(feature = "native")]
3306            BuiltinId::AiComplete => {
3307                if args.is_empty() {
3308                    return Err(runtime_err(
3309                        "ai_complete() expects at least 1 argument (prompt)",
3310                    ));
3311                }
3312                let prompt = match &args[0] {
3313                    VmValue::String(s) => s.to_string(),
3314                    _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3315                };
3316                let model = match args.get(1) {
3317                    Some(VmValue::String(s)) => Some(s.to_string()),
3318                    _ => None,
3319                };
3320                let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3321                    .map_err(|e| runtime_err(e.to_string()))?;
3322                Ok(VmValue::String(Arc::from(result.as_str())))
3323            }
3324            #[cfg(feature = "native")]
3325            BuiltinId::AiChat => {
3326                if args.is_empty() {
3327                    return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3328                }
3329                let model = match &args[0] {
3330                    VmValue::String(s) => s.to_string(),
3331                    _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3332                };
3333                let system = match args.get(1) {
3334                    Some(VmValue::String(s)) => Some(s.to_string()),
3335                    _ => None,
3336                };
3337                let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3338                {
3339                    msgs.chunks(2)
3340                        .filter_map(|chunk| {
3341                            if chunk.len() == 2
3342                                && let (VmValue::String(role), VmValue::String(content)) =
3343                                    (&chunk[0], &chunk[1])
3344                            {
3345                                return Some((role.to_string(), content.to_string()));
3346                            }
3347                            None
3348                        })
3349                        .collect()
3350                } else {
3351                    Vec::new()
3352                };
3353                let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3354                    .map_err(|e| runtime_err(e.to_string()))?;
3355                Ok(VmValue::String(Arc::from(result.as_str())))
3356            }
3357            #[cfg(feature = "native")]
3358            BuiltinId::ModelSave => {
3359                if args.len() != 2 {
3360                    return Err(runtime_err(
3361                        "model_save() expects 2 arguments (model, path)",
3362                    ));
3363                }
3364                let model = match &args[0] {
3365                    VmValue::Model(m) => m,
3366                    _ => return Err(runtime_err("model_save() first arg must be a model")),
3367                };
3368                let path = match &args[1] {
3369                    VmValue::String(s) => s.to_string(),
3370                    _ => return Err(runtime_err("model_save() second arg must be a string path")),
3371                };
3372                model
3373                    .save(std::path::Path::new(&path))
3374                    .map_err(|e| runtime_err(e.to_string()))?;
3375                Ok(VmValue::None)
3376            }
3377            #[cfg(feature = "native")]
3378            BuiltinId::ModelLoad => {
3379                if args.is_empty() {
3380                    return Err(runtime_err("model_load() expects 1 argument (path)"));
3381                }
3382                let path = match &args[0] {
3383                    VmValue::String(s) => s.to_string(),
3384                    _ => return Err(runtime_err("model_load() arg must be a string path")),
3385                };
3386                let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3387                    .map_err(|e| runtime_err(e.to_string()))?;
3388                Ok(VmValue::Model(Arc::new(model)))
3389            }
3390            #[cfg(feature = "native")]
3391            BuiltinId::ModelRegister => {
3392                if args.len() != 2 {
3393                    return Err(runtime_err(
3394                        "model_register() expects 2 arguments (name, model)",
3395                    ));
3396                }
3397                let name = match &args[0] {
3398                    VmValue::String(s) => s.to_string(),
3399                    _ => return Err(runtime_err("model_register() first arg must be a string")),
3400                };
3401                let model = match &args[1] {
3402                    VmValue::Model(m) => (**m).clone(),
3403                    _ => return Err(runtime_err("model_register() second arg must be a model")),
3404                };
3405                let registry = tl_ai::ModelRegistry::default_location();
3406                registry
3407                    .register(&name, &model)
3408                    .map_err(|e| runtime_err(e.to_string()))?;
3409                Ok(VmValue::None)
3410            }
3411            #[cfg(feature = "native")]
3412            BuiltinId::ModelList => {
3413                let registry = tl_ai::ModelRegistry::default_location();
3414                let names = registry.list();
3415                let items: Vec<VmValue> = names
3416                    .into_iter()
3417                    .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3418                    .collect();
3419                Ok(VmValue::List(Box::new(items)))
3420            }
3421            #[cfg(feature = "native")]
3422            BuiltinId::ModelGet => {
3423                if args.is_empty() {
3424                    return Err(runtime_err("model_get() expects 1 argument (name)"));
3425                }
3426                let name = match &args[0] {
3427                    VmValue::String(s) => s.to_string(),
3428                    _ => return Err(runtime_err("model_get() arg must be a string")),
3429                };
3430                let registry = tl_ai::ModelRegistry::default_location();
3431                match registry.get(&name) {
3432                    Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3433                    Err(_) => Ok(VmValue::None),
3434                }
3435            }
3436            #[cfg(not(feature = "native"))]
3437            BuiltinId::Tensor
3438            | BuiltinId::TensorZeros
3439            | BuiltinId::TensorOnes
3440            | BuiltinId::TensorShape
3441            | BuiltinId::TensorReshape
3442            | BuiltinId::TensorTranspose
3443            | BuiltinId::TensorSum
3444            | BuiltinId::TensorMean
3445            | BuiltinId::TensorDot
3446            | BuiltinId::Predict
3447            | BuiltinId::Similarity
3448            | BuiltinId::AiComplete
3449            | BuiltinId::AiChat
3450            | BuiltinId::ModelSave
3451            | BuiltinId::ModelLoad
3452            | BuiltinId::ModelRegister
3453            | BuiltinId::ModelList
3454            | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3455            // Streaming builtins
3456            #[cfg(feature = "native")]
3457            BuiltinId::AlertSlack => {
3458                if args.len() < 2 {
3459                    return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3460                }
3461                let url = match &args[0] {
3462                    VmValue::String(s) => s.to_string(),
3463                    _ => return Err(runtime_err("alert_slack: url must be a string")),
3464                };
3465                let msg = format!("{}", args[1]);
3466                tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3467                    .map_err(|e| runtime_err(&e))?;
3468                Ok(VmValue::None)
3469            }
3470            #[cfg(feature = "native")]
3471            BuiltinId::AlertWebhook => {
3472                if args.len() < 2 {
3473                    return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3474                }
3475                let url = match &args[0] {
3476                    VmValue::String(s) => s.to_string(),
3477                    _ => return Err(runtime_err("alert_webhook: url must be a string")),
3478                };
3479                let msg = format!("{}", args[1]);
3480                tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3481                    .map_err(|e| runtime_err(&e))?;
3482                Ok(VmValue::None)
3483            }
3484            #[cfg(feature = "native")]
3485            BuiltinId::Emit => {
3486                if args.is_empty() {
3487                    return Err(runtime_err("emit() requires at least 1 argument"));
3488                }
3489                self.output.push(format!("emit: {}", args[0]));
3490                Ok(args[0].clone())
3491            }
3492            #[cfg(feature = "native")]
3493            BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3494            #[cfg(feature = "native")]
3495            BuiltinId::RunPipeline => {
3496                if args.is_empty() {
3497                    return Err(runtime_err("run_pipeline() requires a pipeline"));
3498                }
3499                if let VmValue::PipelineDef(ref def) = args[0] {
3500                    Ok(VmValue::String(Arc::from(
3501                        format!("Pipeline '{}' triggered", def.name).as_str(),
3502                    )))
3503                } else {
3504                    Err(runtime_err("run_pipeline: argument must be a pipeline"))
3505                }
3506            }
3507            #[cfg(not(feature = "native"))]
3508            BuiltinId::AlertSlack
3509            | BuiltinId::AlertWebhook
3510            | BuiltinId::Emit
3511            | BuiltinId::Lineage
3512            | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3513            // Phase 5: Math builtins
3514            BuiltinId::Sqrt => match args.first() {
3515                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3516                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3517                _ => Err(runtime_err("sqrt() expects a number")),
3518            },
3519            BuiltinId::Pow => {
3520                if args.len() == 2 {
3521                    match (&args[0], &args[1]) {
3522                        (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3523                        (VmValue::Int(a), VmValue::Int(b)) => {
3524                            Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3525                        }
3526                        (VmValue::Float(a), VmValue::Int(b)) => {
3527                            Ok(VmValue::Float(a.powf(*b as f64)))
3528                        }
3529                        (VmValue::Int(a), VmValue::Float(b)) => {
3530                            Ok(VmValue::Float((*a as f64).powf(*b)))
3531                        }
3532                        _ => Err(runtime_err("pow() expects two numbers")),
3533                    }
3534                } else {
3535                    Err(runtime_err("pow() expects 2 arguments"))
3536                }
3537            }
3538            BuiltinId::Floor => match args.first() {
3539                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3540                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3541                _ => Err(runtime_err("floor() expects a number")),
3542            },
3543            BuiltinId::Ceil => match args.first() {
3544                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3545                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3546                _ => Err(runtime_err("ceil() expects a number")),
3547            },
3548            BuiltinId::Round => match args.first() {
3549                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3550                Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3551                _ => Err(runtime_err("round() expects a number")),
3552            },
3553            BuiltinId::Sin => match args.first() {
3554                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3555                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3556                _ => Err(runtime_err("sin() expects a number")),
3557            },
3558            BuiltinId::Cos => match args.first() {
3559                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3560                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3561                _ => Err(runtime_err("cos() expects a number")),
3562            },
3563            BuiltinId::Tan => match args.first() {
3564                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3565                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3566                _ => Err(runtime_err("tan() expects a number")),
3567            },
3568            BuiltinId::Log => match args.first() {
3569                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3570                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3571                _ => Err(runtime_err("log() expects a number")),
3572            },
3573            BuiltinId::Log2 => match args.first() {
3574                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3575                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3576                _ => Err(runtime_err("log2() expects a number")),
3577            },
3578            BuiltinId::Log10 => match args.first() {
3579                Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3580                Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3581                _ => Err(runtime_err("log10() expects a number")),
3582            },
3583            BuiltinId::Join => {
3584                if args.len() == 2 {
3585                    if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3586                        let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3587                        Ok(VmValue::String(Arc::from(
3588                            parts.join(sep.as_ref()).as_str(),
3589                        )))
3590                    } else {
3591                        Err(runtime_err("join() expects separator and list"))
3592                    }
3593                } else {
3594                    Err(runtime_err("join() expects 2 arguments"))
3595                }
3596            }
3597            #[cfg(feature = "native")]
3598            BuiltinId::HttpGet => {
3599                self.check_permission("network")?;
3600                if args.is_empty() {
3601                    return Err(runtime_err("http_get() expects a URL"));
3602                }
3603                if let VmValue::String(url) = &args[0] {
3604                    match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3605                        Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3606                        Err(e) => {
3607                            let msg = format!("HTTP GET error: {e}");
3608                            self.thrown_value =
3609                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3610                                    type_name: Arc::from("NetworkError"),
3611                                    variant: Arc::from("HttpError"),
3612                                    fields: vec![
3613                                        VmValue::String(Arc::from(msg.as_str())),
3614                                        VmValue::String(url.clone()),
3615                                    ],
3616                                })));
3617                            Err(runtime_err(msg))
3618                        }
3619                    }
3620                } else {
3621                    Err(runtime_err("http_get() expects a string URL"))
3622                }
3623            }
3624            #[cfg(feature = "native")]
3625            BuiltinId::HttpPost => {
3626                self.check_permission("network")?;
3627                if args.len() < 2 {
3628                    return Err(runtime_err("http_post() expects URL and body"));
3629                }
3630                if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3631                    let client = reqwest::blocking::Client::new();
3632                    match client
3633                        .post(url.as_ref())
3634                        .header("Content-Type", "application/json")
3635                        .body(body.to_string())
3636                        .send()
3637                        .and_then(|r| r.text())
3638                    {
3639                        Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
3640                        Err(e) => {
3641                            let msg = format!("HTTP POST error: {e}");
3642                            self.thrown_value =
3643                                Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3644                                    type_name: Arc::from("NetworkError"),
3645                                    variant: Arc::from("HttpError"),
3646                                    fields: vec![
3647                                        VmValue::String(Arc::from(msg.as_str())),
3648                                        VmValue::String(url.clone()),
3649                                    ],
3650                                })));
3651                            Err(runtime_err(msg))
3652                        }
3653                    }
3654                } else {
3655                    Err(runtime_err("http_post() expects string URL and body"))
3656                }
3657            }
3658            #[cfg(not(feature = "native"))]
3659            BuiltinId::HttpGet | BuiltinId::HttpPost => {
3660                Err(runtime_err("HTTP requests not available in WASM"))
3661            }
3662            BuiltinId::Assert => {
3663                if args.is_empty() {
3664                    return Err(runtime_err("assert() expects at least 1 argument"));
3665                }
3666                if !args[0].is_truthy() {
3667                    let msg = if args.len() > 1 {
3668                        format!("{}", args[1])
3669                    } else {
3670                        "Assertion failed".to_string()
3671                    };
3672                    Err(runtime_err(msg))
3673                } else {
3674                    Ok(VmValue::None)
3675                }
3676            }
3677            BuiltinId::AssertEq => {
3678                if args.len() < 2 {
3679                    return Err(runtime_err("assert_eq() expects 2 arguments"));
3680                }
3681                let eq = match (&args[0], &args[1]) {
3682                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
3683                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
3684                    (VmValue::String(a), VmValue::String(b)) => a == b,
3685                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
3686                    (VmValue::None, VmValue::None) => true,
3687                    _ => false,
3688                };
3689                if !eq {
3690                    Err(runtime_err(format!(
3691                        "Assertion failed: {} != {}",
3692                        args[0], args[1]
3693                    )))
3694                } else {
3695                    Ok(VmValue::None)
3696                }
3697            }
3698            // ── Phase 6: Stdlib & Ecosystem builtins ──
3699            BuiltinId::JsonParse => {
3700                if args.is_empty() {
3701                    return Err(runtime_err("json_parse() expects a string"));
3702                }
3703                if let VmValue::String(s) = &args[0] {
3704                    let json_val: serde_json::Value = serde_json::from_str(s)
3705                        .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
3706                    Ok(vm_json_to_value(&json_val))
3707                } else {
3708                    Err(runtime_err("json_parse() expects a string"))
3709                }
3710            }
3711            BuiltinId::JsonStringify => {
3712                if args.is_empty() {
3713                    return Err(runtime_err("json_stringify() expects a value"));
3714                }
3715                let json = vm_value_to_json(&args[0]);
3716                Ok(VmValue::String(Arc::from(json.to_string().as_str())))
3717            }
3718            BuiltinId::MapFrom => {
3719                if !args.len().is_multiple_of(2) {
3720                    return Err(runtime_err(
3721                        "map_from() expects even number of arguments (key, value pairs)",
3722                    ));
3723                }
3724                let mut pairs = Vec::new();
3725                for chunk in args.chunks(2) {
3726                    let key = match &chunk[0] {
3727                        VmValue::String(s) => s.clone(),
3728                        other => Arc::from(format!("{other}").as_str()),
3729                    };
3730                    pairs.push((key, chunk[1].clone()));
3731                }
3732                Ok(VmValue::Map(Box::new(pairs)))
3733            }
3734            #[cfg(feature = "native")]
3735            BuiltinId::ReadFile => {
3736                self.check_permission("file_read")?;
3737                if args.is_empty() {
3738                    return Err(runtime_err("read_file() expects a path"));
3739                }
3740                if let VmValue::String(path) = &args[0] {
3741                    let content = std::fs::read_to_string(path.as_ref())
3742                        .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
3743                    Ok(VmValue::String(Arc::from(content.as_str())))
3744                } else {
3745                    Err(runtime_err("read_file() expects a string path"))
3746                }
3747            }
3748            #[cfg(feature = "native")]
3749            BuiltinId::WriteFile => {
3750                self.check_permission("file_write")?;
3751                if args.len() < 2 {
3752                    return Err(runtime_err("write_file() expects path and content"));
3753                }
3754                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3755                    std::fs::write(path.as_ref(), content.as_ref())
3756                        .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
3757                    Ok(VmValue::None)
3758                } else {
3759                    Err(runtime_err("write_file() expects string path and content"))
3760                }
3761            }
3762            #[cfg(feature = "native")]
3763            BuiltinId::AppendFile => {
3764                self.check_permission("file_write")?;
3765                if args.len() < 2 {
3766                    return Err(runtime_err("append_file() expects path and content"));
3767                }
3768                if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
3769                    use std::io::Write;
3770                    let mut file = std::fs::OpenOptions::new()
3771                        .create(true)
3772                        .append(true)
3773                        .open(path.as_ref())
3774                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3775                    file.write_all(content.as_bytes())
3776                        .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
3777                    Ok(VmValue::None)
3778                } else {
3779                    Err(runtime_err("append_file() expects string path and content"))
3780                }
3781            }
3782            #[cfg(feature = "native")]
3783            BuiltinId::FileExists => {
3784                self.check_permission("file_read")?;
3785                if args.is_empty() {
3786                    return Err(runtime_err("file_exists() expects a path"));
3787                }
3788                if let VmValue::String(path) = &args[0] {
3789                    Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
3790                } else {
3791                    Err(runtime_err("file_exists() expects a string path"))
3792                }
3793            }
3794            #[cfg(feature = "native")]
3795            BuiltinId::ListDir => {
3796                self.check_permission("file_read")?;
3797                if args.is_empty() {
3798                    return Err(runtime_err("list_dir() expects a path"));
3799                }
3800                if let VmValue::String(path) = &args[0] {
3801                    let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
3802                        .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
3803                        .filter_map(|e| e.ok())
3804                        .map(|e| {
3805                            VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
3806                        })
3807                        .collect();
3808                    Ok(VmValue::List(Box::new(entries)))
3809                } else {
3810                    Err(runtime_err("list_dir() expects a string path"))
3811                }
3812            }
3813            #[cfg(not(feature = "native"))]
3814            BuiltinId::ReadFile
3815            | BuiltinId::WriteFile
3816            | BuiltinId::AppendFile
3817            | BuiltinId::FileExists
3818            | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
3819            #[cfg(feature = "native")]
3820            BuiltinId::EnvGet => {
3821                if args.is_empty() {
3822                    return Err(runtime_err("env_get() expects a name"));
3823                }
3824                if let VmValue::String(name) = &args[0] {
3825                    match std::env::var(name.as_ref()) {
3826                        Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
3827                        Err(_) => Ok(VmValue::None),
3828                    }
3829                } else {
3830                    Err(runtime_err("env_get() expects a string"))
3831                }
3832            }
3833            #[cfg(feature = "native")]
3834            BuiltinId::EnvSet => {
3835                self.check_permission("env_write")?;
3836                if args.len() < 2 {
3837                    return Err(runtime_err("env_set() expects name and value"));
3838                }
3839                if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
3840                    let _guard = env_lock();
3841                    unsafe {
3842                        std::env::set_var(name.as_ref(), val.as_ref());
3843                    }
3844                    Ok(VmValue::None)
3845                } else {
3846                    Err(runtime_err("env_set() expects two strings"))
3847                }
3848            }
3849            #[cfg(not(feature = "native"))]
3850            BuiltinId::EnvGet | BuiltinId::EnvSet => {
3851                Err(runtime_err("Environment variables not available in WASM"))
3852            }
3853            BuiltinId::RegexMatch => {
3854                if args.len() < 2 {
3855                    return Err(runtime_err("regex_match() expects pattern and string"));
3856                }
3857                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3858                    if pattern.len() > 10_000 {
3859                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3860                    }
3861                    let re = regex::RegexBuilder::new(pattern)
3862                        .size_limit(10_000_000)
3863                        .build()
3864                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3865                    Ok(VmValue::Bool(re.is_match(text)))
3866                } else {
3867                    Err(runtime_err(
3868                        "regex_match() expects string pattern and string",
3869                    ))
3870                }
3871            }
3872            BuiltinId::RegexFind => {
3873                if args.len() < 2 {
3874                    return Err(runtime_err("regex_find() expects pattern and string"));
3875                }
3876                if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
3877                    if pattern.len() > 10_000 {
3878                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3879                    }
3880                    let re = regex::RegexBuilder::new(pattern)
3881                        .size_limit(10_000_000)
3882                        .build()
3883                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3884                    let matches: Vec<VmValue> = re
3885                        .find_iter(text)
3886                        .map(|m| VmValue::String(Arc::from(m.as_str())))
3887                        .collect();
3888                    Ok(VmValue::List(Box::new(matches)))
3889                } else {
3890                    Err(runtime_err(
3891                        "regex_find() expects string pattern and string",
3892                    ))
3893                }
3894            }
3895            BuiltinId::RegexReplace => {
3896                if args.len() < 3 {
3897                    return Err(runtime_err(
3898                        "regex_replace() expects pattern, string, replacement",
3899                    ));
3900                }
3901                if let (
3902                    VmValue::String(pattern),
3903                    VmValue::String(text),
3904                    VmValue::String(replacement),
3905                ) = (&args[0], &args[1], &args[2])
3906                {
3907                    if pattern.len() > 10_000 {
3908                        return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
3909                    }
3910                    let re = regex::RegexBuilder::new(pattern)
3911                        .size_limit(10_000_000)
3912                        .build()
3913                        .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
3914                    Ok(VmValue::String(Arc::from(
3915                        re.replace_all(text, replacement.as_ref()).as_ref(),
3916                    )))
3917                } else {
3918                    Err(runtime_err("regex_replace() expects three strings"))
3919                }
3920            }
3921            BuiltinId::Now => {
3922                let ts = chrono::Utc::now().timestamp_millis();
3923                Ok(VmValue::DateTime(ts))
3924            }
3925            BuiltinId::DateFormat => {
3926                if args.len() < 2 {
3927                    return Err(runtime_err(
3928                        "date_format() expects datetime/timestamp and format",
3929                    ));
3930                }
3931                let ts = match &args[0] {
3932                    VmValue::DateTime(ms) => *ms,
3933                    VmValue::Int(ms) => *ms,
3934                    _ => {
3935                        return Err(runtime_err(
3936                            "date_format() expects a datetime or int timestamp",
3937                        ));
3938                    }
3939                };
3940                let fmt = match &args[1] {
3941                    VmValue::String(s) => s,
3942                    _ => return Err(runtime_err("date_format() expects a string format")),
3943                };
3944                use chrono::TimeZone;
3945                let secs = ts / 1000;
3946                let nsecs = ((ts % 1000) * 1_000_000) as u32;
3947                let dt = chrono::Utc
3948                    .timestamp_opt(secs, nsecs)
3949                    .single()
3950                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
3951                Ok(VmValue::String(Arc::from(
3952                    dt.format(fmt.as_ref()).to_string().as_str(),
3953                )))
3954            }
3955            BuiltinId::DateParse => {
3956                if args.len() < 2 {
3957                    return Err(runtime_err("date_parse() expects string and format"));
3958                }
3959                if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
3960                    let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
3961                        .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
3962                    let ts = dt.and_utc().timestamp_millis();
3963                    Ok(VmValue::DateTime(ts))
3964                } else {
3965                    Err(runtime_err("date_parse() expects two strings"))
3966                }
3967            }
3968            BuiltinId::Zip => {
3969                if args.len() < 2 {
3970                    return Err(runtime_err("zip() expects two lists"));
3971                }
3972                if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
3973                    let pairs: Vec<VmValue> = a
3974                        .iter()
3975                        .zip(b.iter())
3976                        .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
3977                        .collect();
3978                    Ok(VmValue::List(Box::new(pairs)))
3979                } else {
3980                    Err(runtime_err("zip() expects two lists"))
3981                }
3982            }
3983            BuiltinId::Enumerate => {
3984                if args.is_empty() {
3985                    return Err(runtime_err("enumerate() expects a list"));
3986                }
3987                if let VmValue::List(items) = &args[0] {
3988                    let pairs: Vec<VmValue> = items
3989                        .iter()
3990                        .enumerate()
3991                        .map(|(i, v)| {
3992                            VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
3993                        })
3994                        .collect();
3995                    Ok(VmValue::List(Box::new(pairs)))
3996                } else {
3997                    Err(runtime_err("enumerate() expects a list"))
3998                }
3999            }
4000            BuiltinId::Bool => {
4001                if args.is_empty() {
4002                    return Err(runtime_err("bool() expects a value"));
4003                }
4004                Ok(VmValue::Bool(args[0].is_truthy()))
4005            }
4006
4007            // Phase 7: Concurrency builtins
4008            #[cfg(feature = "native")]
4009            BuiltinId::Spawn => {
4010                if args.is_empty() {
4011                    return Err(runtime_err("spawn() expects a function argument"));
4012                }
4013                match &args[0] {
4014                    VmValue::Function(closure) => {
4015                        let proto = closure.prototype.clone();
4016                        // Close all upvalues (convert Open → Closed with current values)
4017                        let mut closed_upvalues = Vec::new();
4018                        for uv in &closure.upvalues {
4019                            match uv {
4020                                UpvalueRef::Open { stack_index } => {
4021                                    let val = self.stack[*stack_index].clone();
4022                                    closed_upvalues.push(UpvalueRef::Closed(val));
4023                                }
4024                                UpvalueRef::Closed(v) => {
4025                                    closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4026                                }
4027                            }
4028                        }
4029                        let globals = self.globals.clone();
4030                        let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4031
4032                        std::thread::spawn(move || {
4033                            let mut vm = Vm::new();
4034                            vm.globals = globals;
4035                            let result = vm.execute_closure(&proto, &closed_upvalues);
4036                            let _ = tx.send(result.map_err(|e| match e {
4037                                TlError::Runtime(re) => re.message,
4038                                other => format!("{other}"),
4039                            }));
4040                        });
4041
4042                        Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4043                    }
4044                    _ => Err(runtime_err("spawn() expects a function")),
4045                }
4046            }
4047            #[cfg(feature = "native")]
4048            BuiltinId::Sleep => {
4049                if args.is_empty() {
4050                    return Err(runtime_err("sleep() expects a duration in milliseconds"));
4051                }
4052                match &args[0] {
4053                    VmValue::Int(ms) => {
4054                        std::thread::sleep(Duration::from_millis(*ms as u64));
4055                        Ok(VmValue::None)
4056                    }
4057                    _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4058                }
4059            }
4060            #[cfg(feature = "native")]
4061            BuiltinId::Channel => {
4062                let capacity = match args.first() {
4063                    Some(VmValue::Int(n)) => *n as usize,
4064                    None => 64,
4065                    _ => {
4066                        return Err(runtime_err(
4067                            "channel() expects an optional integer capacity",
4068                        ));
4069                    }
4070                };
4071                Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4072            }
4073            #[cfg(feature = "native")]
4074            BuiltinId::Send => {
4075                if args.len() < 2 {
4076                    return Err(runtime_err("send() expects a channel and a value"));
4077                }
4078                match &args[0] {
4079                    VmValue::Channel(ch) => {
4080                        ch.sender
4081                            .send(args[1].clone())
4082                            .map_err(|_| runtime_err("Channel disconnected"))?;
4083                        Ok(VmValue::None)
4084                    }
4085                    _ => Err(runtime_err("send() expects a channel as first argument")),
4086                }
4087            }
4088            #[cfg(feature = "native")]
4089            BuiltinId::Recv => {
4090                if args.is_empty() {
4091                    return Err(runtime_err("recv() expects a channel"));
4092                }
4093                match &args[0] {
4094                    VmValue::Channel(ch) => {
4095                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4096                        match guard.recv() {
4097                            Ok(val) => Ok(val),
4098                            Err(_) => Ok(VmValue::None),
4099                        }
4100                    }
4101                    _ => Err(runtime_err("recv() expects a channel")),
4102                }
4103            }
4104            #[cfg(feature = "native")]
4105            BuiltinId::TryRecv => {
4106                if args.is_empty() {
4107                    return Err(runtime_err("try_recv() expects a channel"));
4108                }
4109                match &args[0] {
4110                    VmValue::Channel(ch) => {
4111                        let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4112                        match guard.try_recv() {
4113                            Ok(val) => Ok(val),
4114                            Err(_) => Ok(VmValue::None),
4115                        }
4116                    }
4117                    _ => Err(runtime_err("try_recv() expects a channel")),
4118                }
4119            }
4120            #[cfg(feature = "native")]
4121            BuiltinId::AwaitAll => {
4122                if args.is_empty() {
4123                    return Err(runtime_err("await_all() expects a list of tasks"));
4124                }
4125                match &args[0] {
4126                    VmValue::List(tasks) => {
4127                        let mut results = Vec::with_capacity(tasks.len());
4128                        for task in tasks.iter() {
4129                            match task {
4130                                VmValue::Task(t) => {
4131                                    let rx = {
4132                                        let mut guard =
4133                                            t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4134                                        guard.take()
4135                                    };
4136                                    match rx {
4137                                        Some(receiver) => match receiver.recv() {
4138                                            Ok(Ok(val)) => results.push(val),
4139                                            Ok(Err(e)) => return Err(runtime_err(e)),
4140                                            Err(_) => {
4141                                                return Err(runtime_err(
4142                                                    "Task channel disconnected",
4143                                                ));
4144                                            }
4145                                        },
4146                                        None => return Err(runtime_err("Task already awaited")),
4147                                    }
4148                                }
4149                                other => results.push(other.clone()),
4150                            }
4151                        }
4152                        Ok(VmValue::List(Box::new(results)))
4153                    }
4154                    _ => Err(runtime_err("await_all() expects a list")),
4155                }
4156            }
4157            #[cfg(feature = "native")]
4158            BuiltinId::Pmap => {
4159                if args.len() < 2 {
4160                    return Err(runtime_err("pmap() expects a list and a function"));
4161                }
4162                let items = match &args[0] {
4163                    VmValue::List(items) => (**items).clone(),
4164                    _ => return Err(runtime_err("pmap() expects a list as first argument")),
4165                };
4166                let closure = match &args[1] {
4167                    VmValue::Function(c) => c.clone(),
4168                    _ => return Err(runtime_err("pmap() expects a function as second argument")),
4169                };
4170
4171                // Close all upvalues
4172                let mut closed_upvalues = Vec::new();
4173                for uv in &closure.upvalues {
4174                    match uv {
4175                        UpvalueRef::Open { stack_index } => {
4176                            let val = self.stack[*stack_index].clone();
4177                            closed_upvalues.push(UpvalueRef::Closed(val));
4178                        }
4179                        UpvalueRef::Closed(v) => {
4180                            closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4181                        }
4182                    }
4183                }
4184
4185                let proto = closure.prototype.clone();
4186                let globals = self.globals.clone();
4187
4188                // Spawn one thread per item
4189                let mut handles = Vec::with_capacity(items.len());
4190                for item in items {
4191                    let proto = proto.clone();
4192                    let upvalues = closed_upvalues.clone();
4193                    let globals = globals.clone();
4194                    let handle = std::thread::spawn(move || {
4195                        let mut vm = Vm::new();
4196                        vm.globals = globals;
4197                        vm.execute_closure_with_args(&proto, &upvalues, &[item])
4198                            .map_err(|e| match e {
4199                                TlError::Runtime(re) => re.message,
4200                                other => format!("{other}"),
4201                            })
4202                    });
4203                    handles.push(handle);
4204                }
4205
4206                let mut results = Vec::with_capacity(handles.len());
4207                for handle in handles {
4208                    match handle.join() {
4209                        Ok(Ok(val)) => results.push(val),
4210                        Ok(Err(e)) => return Err(runtime_err(e)),
4211                        Err(_) => return Err(runtime_err("pmap() thread panicked")),
4212                    }
4213                }
4214                Ok(VmValue::List(Box::new(results)))
4215            }
4216            #[cfg(feature = "native")]
4217            BuiltinId::Timeout => {
4218                if args.len() < 2 {
4219                    return Err(runtime_err(
4220                        "timeout() expects a task and a duration in milliseconds",
4221                    ));
4222                }
4223                let ms = match &args[1] {
4224                    VmValue::Int(n) => *n as u64,
4225                    _ => return Err(runtime_err("timeout() expects an integer duration")),
4226                };
4227                match &args[0] {
4228                    VmValue::Task(task) => {
4229                        let rx = {
4230                            let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4231                            guard.take()
4232                        };
4233                        match rx {
4234                            Some(receiver) => {
4235                                match receiver.recv_timeout(Duration::from_millis(ms)) {
4236                                    Ok(Ok(val)) => Ok(val),
4237                                    Ok(Err(e)) => Err(runtime_err(e)),
4238                                    Err(mpsc::RecvTimeoutError::Timeout) => {
4239                                        Err(runtime_err("Task timed out"))
4240                                    }
4241                                    Err(mpsc::RecvTimeoutError::Disconnected) => {
4242                                        Err(runtime_err("Task channel disconnected"))
4243                                    }
4244                                }
4245                            }
4246                            None => Err(runtime_err("Task already awaited")),
4247                        }
4248                    }
4249                    _ => Err(runtime_err("timeout() expects a task as first argument")),
4250                }
4251            }
4252            #[cfg(not(feature = "native"))]
4253            BuiltinId::Spawn
4254            | BuiltinId::Sleep
4255            | BuiltinId::Channel
4256            | BuiltinId::Send
4257            | BuiltinId::Recv
4258            | BuiltinId::TryRecv
4259            | BuiltinId::AwaitAll
4260            | BuiltinId::Pmap
4261            | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4262            // Phase 8: Iterators & Generators
4263            BuiltinId::Next => {
4264                if args.is_empty() {
4265                    return Err(runtime_err("next() expects a generator"));
4266                }
4267                match &args[0] {
4268                    VmValue::Generator(gen_arc) => {
4269                        let g = gen_arc.clone();
4270                        self.generator_next(&g)
4271                    }
4272                    _ => Err(runtime_err("next() expects a generator")),
4273                }
4274            }
4275            BuiltinId::IsGenerator => {
4276                let val = args.first().unwrap_or(&VmValue::None);
4277                Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4278            }
4279            BuiltinId::Iter => {
4280                if args.is_empty() {
4281                    return Err(runtime_err("iter() expects a list"));
4282                }
4283                match &args[0] {
4284                    VmValue::List(items) => {
4285                        let gn = VmGenerator::new(GeneratorKind::ListIter {
4286                            items: (**items).clone(),
4287                            index: 0,
4288                        });
4289                        Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4290                    }
4291                    _ => Err(runtime_err("iter() expects a list")),
4292                }
4293            }
4294            BuiltinId::Take => {
4295                if args.len() < 2 {
4296                    return Err(runtime_err("take() expects a generator and a count"));
4297                }
4298                let gen_arc = match &args[0] {
4299                    VmValue::Generator(g) => g.clone(),
4300                    _ => return Err(runtime_err("take() expects a generator as first argument")),
4301                };
4302                let n = match &args[1] {
4303                    VmValue::Int(n) => *n as usize,
4304                    _ => return Err(runtime_err("take() expects an integer count")),
4305                };
4306                let gn = VmGenerator::new(GeneratorKind::Take {
4307                    source: gen_arc,
4308                    remaining: n,
4309                });
4310                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4311            }
4312            BuiltinId::Skip_ => {
4313                if args.len() < 2 {
4314                    return Err(runtime_err("skip() expects a generator and a count"));
4315                }
4316                let gen_arc = match &args[0] {
4317                    VmValue::Generator(g) => g.clone(),
4318                    _ => return Err(runtime_err("skip() expects a generator as first argument")),
4319                };
4320                let n = match &args[1] {
4321                    VmValue::Int(n) => *n as usize,
4322                    _ => return Err(runtime_err("skip() expects an integer count")),
4323                };
4324                let gn = VmGenerator::new(GeneratorKind::Skip {
4325                    source: gen_arc,
4326                    remaining: n,
4327                });
4328                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4329            }
4330            BuiltinId::GenCollect => {
4331                if args.is_empty() {
4332                    return Err(runtime_err("gen_collect() expects a generator"));
4333                }
4334                match &args[0] {
4335                    VmValue::Generator(gen_arc) => {
4336                        let g = gen_arc.clone();
4337                        let mut items = Vec::new();
4338                        loop {
4339                            let val = self.generator_next(&g)?;
4340                            if matches!(val, VmValue::None) {
4341                                break;
4342                            }
4343                            items.push(val);
4344                        }
4345                        Ok(VmValue::List(Box::new(items)))
4346                    }
4347                    _ => Err(runtime_err("gen_collect() expects a generator")),
4348                }
4349            }
4350            BuiltinId::GenMap => {
4351                if args.len() < 2 {
4352                    return Err(runtime_err("gen_map() expects a generator and a function"));
4353                }
4354                let gen_arc = match &args[0] {
4355                    VmValue::Generator(g) => g.clone(),
4356                    _ => {
4357                        return Err(runtime_err(
4358                            "gen_map() expects a generator as first argument",
4359                        ));
4360                    }
4361                };
4362                let func = args[1].clone();
4363                let gn = VmGenerator::new(GeneratorKind::Map {
4364                    source: gen_arc,
4365                    func,
4366                });
4367                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4368            }
4369            BuiltinId::GenFilter => {
4370                if args.len() < 2 {
4371                    return Err(runtime_err(
4372                        "gen_filter() expects a generator and a function",
4373                    ));
4374                }
4375                let gen_arc = match &args[0] {
4376                    VmValue::Generator(g) => g.clone(),
4377                    _ => {
4378                        return Err(runtime_err(
4379                            "gen_filter() expects a generator as first argument",
4380                        ));
4381                    }
4382                };
4383                let func = args[1].clone();
4384                let gn = VmGenerator::new(GeneratorKind::Filter {
4385                    source: gen_arc,
4386                    func,
4387                });
4388                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4389            }
4390            BuiltinId::Chain => {
4391                if args.len() < 2 {
4392                    return Err(runtime_err("chain() expects two generators"));
4393                }
4394                let first = match &args[0] {
4395                    VmValue::Generator(g) => g.clone(),
4396                    _ => return Err(runtime_err("chain() expects generators")),
4397                };
4398                let second = match &args[1] {
4399                    VmValue::Generator(g) => g.clone(),
4400                    _ => return Err(runtime_err("chain() expects generators")),
4401                };
4402                let gn = VmGenerator::new(GeneratorKind::Chain {
4403                    first,
4404                    second,
4405                    on_second: false,
4406                });
4407                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4408            }
4409            BuiltinId::GenZip => {
4410                if args.len() < 2 {
4411                    return Err(runtime_err("gen_zip() expects two generators"));
4412                }
4413                let first = match &args[0] {
4414                    VmValue::Generator(g) => g.clone(),
4415                    _ => return Err(runtime_err("gen_zip() expects generators")),
4416                };
4417                let second = match &args[1] {
4418                    VmValue::Generator(g) => g.clone(),
4419                    _ => return Err(runtime_err("gen_zip() expects generators")),
4420                };
4421                let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4422                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4423            }
4424            BuiltinId::GenEnumerate => {
4425                if args.is_empty() {
4426                    return Err(runtime_err("gen_enumerate() expects a generator"));
4427                }
4428                let gen_arc = match &args[0] {
4429                    VmValue::Generator(g) => g.clone(),
4430                    _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4431                };
4432                let gn = VmGenerator::new(GeneratorKind::Enumerate {
4433                    source: gen_arc,
4434                    index: 0,
4435                });
4436                Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4437            }
4438            // Phase 10: Result builtins
4439            BuiltinId::Ok => {
4440                let val = if args.is_empty() {
4441                    VmValue::None
4442                } else {
4443                    args[0].clone()
4444                };
4445                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4446                    type_name: Arc::from("Result"),
4447                    variant: Arc::from("Ok"),
4448                    fields: vec![val],
4449                })))
4450            }
4451            BuiltinId::Err_ => {
4452                let val = if args.is_empty() {
4453                    VmValue::String(Arc::from("error"))
4454                } else {
4455                    args[0].clone()
4456                };
4457                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4458                    type_name: Arc::from("Result"),
4459                    variant: Arc::from("Err"),
4460                    fields: vec![val],
4461                })))
4462            }
4463            BuiltinId::IsOk => {
4464                if args.is_empty() {
4465                    return Err(runtime_err("is_ok() expects an argument"));
4466                }
4467                match &args[0] {
4468                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4469                        Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4470                    }
4471                    _ => Ok(VmValue::Bool(false)),
4472                }
4473            }
4474            BuiltinId::IsErr => {
4475                if args.is_empty() {
4476                    return Err(runtime_err("is_err() expects an argument"));
4477                }
4478                match &args[0] {
4479                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4480                        Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4481                    }
4482                    _ => Ok(VmValue::Bool(false)),
4483                }
4484            }
4485            BuiltinId::Unwrap => {
4486                if args.is_empty() {
4487                    return Err(runtime_err("unwrap() expects an argument"));
4488                }
4489                match &args[0] {
4490                    VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4491                        if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4492                            Ok(ei.fields[0].clone())
4493                        } else if ei.variant.as_ref() == "Err" {
4494                            let msg = if ei.fields.is_empty() {
4495                                "error".to_string()
4496                            } else {
4497                                format!("{}", ei.fields[0])
4498                            };
4499                            Err(runtime_err(format!("unwrap() called on Err({msg})")))
4500                        } else {
4501                            Ok(VmValue::None)
4502                        }
4503                    }
4504                    VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4505                    other => Ok(other.clone()),
4506                }
4507            }
4508            BuiltinId::SetFrom => {
4509                let list = match args.first() {
4510                    Some(VmValue::List(items)) => items,
4511                    _ => return Err(runtime_err("set_from() expects a list")),
4512                };
4513                if list.is_empty() {
4514                    return Ok(VmValue::Set(Box::default()));
4515                }
4516                let mut result = Vec::new();
4517                for item in list.iter() {
4518                    if !result.iter().any(|x| vm_values_equal(x, item)) {
4519                        result.push(item.clone());
4520                    }
4521                }
4522                Ok(VmValue::Set(Box::new(result)))
4523            }
4524            BuiltinId::SetAdd => {
4525                if args.len() < 2 {
4526                    return Err(runtime_err("set_add() expects 2 arguments"));
4527                }
4528                let val = &args[1];
4529                match &args[0] {
4530                    VmValue::Set(items) => {
4531                        let mut new_items = items.clone();
4532                        if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4533                            new_items.push(val.clone());
4534                        }
4535                        Ok(VmValue::Set(new_items))
4536                    }
4537                    _ => Err(runtime_err("set_add() first argument must be a set")),
4538                }
4539            }
4540            BuiltinId::SetRemove => {
4541                if args.len() < 2 {
4542                    return Err(runtime_err("set_remove() expects 2 arguments"));
4543                }
4544                let val = &args[1];
4545                match &args[0] {
4546                    VmValue::Set(items) => {
4547                        let new_items: Vec<VmValue> = items
4548                            .iter()
4549                            .filter(|x| !vm_values_equal(x, val))
4550                            .cloned()
4551                            .collect();
4552                        Ok(VmValue::Set(Box::new(new_items)))
4553                    }
4554                    _ => Err(runtime_err("set_remove() first argument must be a set")),
4555                }
4556            }
4557            BuiltinId::SetContains => {
4558                if args.len() < 2 {
4559                    return Err(runtime_err("set_contains() expects 2 arguments"));
4560                }
4561                let val = &args[1];
4562                match &args[0] {
4563                    VmValue::Set(items) => {
4564                        Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4565                    }
4566                    _ => Err(runtime_err("set_contains() first argument must be a set")),
4567                }
4568            }
4569            BuiltinId::SetUnion => {
4570                if args.len() < 2 {
4571                    return Err(runtime_err("set_union() expects 2 arguments"));
4572                }
4573                match (&args[0], &args[1]) {
4574                    (VmValue::Set(a), VmValue::Set(b)) => {
4575                        let mut result = a.clone();
4576                        for item in b.iter() {
4577                            if !result.iter().any(|x| vm_values_equal(x, item)) {
4578                                result.push(item.clone());
4579                            }
4580                        }
4581                        Ok(VmValue::Set(result))
4582                    }
4583                    _ => Err(runtime_err("set_union() expects two sets")),
4584                }
4585            }
4586            BuiltinId::SetIntersection => {
4587                if args.len() < 2 {
4588                    return Err(runtime_err("set_intersection() expects 2 arguments"));
4589                }
4590                match (&args[0], &args[1]) {
4591                    (VmValue::Set(a), VmValue::Set(b)) => {
4592                        let result: Vec<VmValue> = a
4593                            .iter()
4594                            .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4595                            .cloned()
4596                            .collect();
4597                        Ok(VmValue::Set(Box::new(result)))
4598                    }
4599                    _ => Err(runtime_err("set_intersection() expects two sets")),
4600                }
4601            }
4602            BuiltinId::SetDifference => {
4603                if args.len() < 2 {
4604                    return Err(runtime_err("set_difference() expects 2 arguments"));
4605                }
4606                match (&args[0], &args[1]) {
4607                    (VmValue::Set(a), VmValue::Set(b)) => {
4608                        let result: Vec<VmValue> = a
4609                            .iter()
4610                            .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4611                            .cloned()
4612                            .collect();
4613                        Ok(VmValue::Set(Box::new(result)))
4614                    }
4615                    _ => Err(runtime_err("set_difference() expects two sets")),
4616                }
4617            }
4618
4619            // ── Phase 15: Data Quality & Connectors ──
4620            #[cfg(feature = "native")]
4621            BuiltinId::FillNull => {
4622                if args.len() < 2 {
4623                    return Err(runtime_err(
4624                        "fill_null() expects (table, column, [strategy], [value])",
4625                    ));
4626                }
4627                let df = match &args[0] {
4628                    VmValue::Table(t) => t.df.clone(),
4629                    _ => return Err(runtime_err("fill_null() first arg must be a table")),
4630                };
4631                let column = match &args[1] {
4632                    VmValue::String(s) => s.to_string(),
4633                    _ => return Err(runtime_err("fill_null() column must be a string")),
4634                };
4635                let strategy = if args.len() > 2 {
4636                    match &args[2] {
4637                        VmValue::String(s) => s.to_string(),
4638                        _ => "value".to_string(),
4639                    }
4640                } else {
4641                    "value".to_string()
4642                };
4643                let fill_value = if args.len() > 3 {
4644                    match &args[3] {
4645                        VmValue::Int(n) => Some(*n as f64),
4646                        VmValue::Float(f) => Some(*f),
4647                        _ => None,
4648                    }
4649                } else if args.len() > 2 && strategy == "value" {
4650                    match &args[2] {
4651                        VmValue::Int(n) => {
4652                            return Ok(VmValue::Table(VmTable {
4653                                df: self
4654                                    .engine()
4655                                    .fill_null(df, &column, "value", Some(*n as f64))
4656                                    .map_err(runtime_err)?,
4657                            }));
4658                        }
4659                        VmValue::Float(f) => {
4660                            return Ok(VmValue::Table(VmTable {
4661                                df: self
4662                                    .engine()
4663                                    .fill_null(df, &column, "value", Some(*f))
4664                                    .map_err(runtime_err)?,
4665                            }));
4666                        }
4667                        _ => None,
4668                    }
4669                } else {
4670                    None
4671                };
4672                let result = self
4673                    .engine()
4674                    .fill_null(df, &column, &strategy, fill_value)
4675                    .map_err(runtime_err)?;
4676                Ok(VmValue::Table(VmTable { df: result }))
4677            }
4678            #[cfg(feature = "native")]
4679            BuiltinId::DropNull => {
4680                if args.len() < 2 {
4681                    return Err(runtime_err("drop_null() expects (table, column)"));
4682                }
4683                let df = match &args[0] {
4684                    VmValue::Table(t) => t.df.clone(),
4685                    _ => return Err(runtime_err("drop_null() first arg must be a table")),
4686                };
4687                let column = match &args[1] {
4688                    VmValue::String(s) => s.to_string(),
4689                    _ => return Err(runtime_err("drop_null() column must be a string")),
4690                };
4691                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
4692                Ok(VmValue::Table(VmTable { df: result }))
4693            }
4694            #[cfg(feature = "native")]
4695            BuiltinId::Dedup => {
4696                if args.is_empty() {
4697                    return Err(runtime_err("dedup() expects (table, [columns...])"));
4698                }
4699                let df = match &args[0] {
4700                    VmValue::Table(t) => t.df.clone(),
4701                    _ => return Err(runtime_err("dedup() first arg must be a table")),
4702                };
4703                let columns: Vec<String> = args[1..]
4704                    .iter()
4705                    .filter_map(|a| {
4706                        if let VmValue::String(s) = a {
4707                            Some(s.to_string())
4708                        } else {
4709                            None
4710                        }
4711                    })
4712                    .collect();
4713                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
4714                Ok(VmValue::Table(VmTable { df: result }))
4715            }
4716            #[cfg(feature = "native")]
4717            BuiltinId::Clamp => {
4718                if args.len() < 4 {
4719                    return Err(runtime_err("clamp() expects (table, column, min, max)"));
4720                }
4721                let df = match &args[0] {
4722                    VmValue::Table(t) => t.df.clone(),
4723                    _ => return Err(runtime_err("clamp() first arg must be a table")),
4724                };
4725                let column = match &args[1] {
4726                    VmValue::String(s) => s.to_string(),
4727                    _ => return Err(runtime_err("clamp() column must be a string")),
4728                };
4729                let min_val = match &args[2] {
4730                    VmValue::Int(n) => *n as f64,
4731                    VmValue::Float(f) => *f,
4732                    _ => return Err(runtime_err("clamp() min must be a number")),
4733                };
4734                let max_val = match &args[3] {
4735                    VmValue::Int(n) => *n as f64,
4736                    VmValue::Float(f) => *f,
4737                    _ => return Err(runtime_err("clamp() max must be a number")),
4738                };
4739                let result = self
4740                    .engine()
4741                    .clamp(df, &column, min_val, max_val)
4742                    .map_err(runtime_err)?;
4743                Ok(VmValue::Table(VmTable { df: result }))
4744            }
4745            #[cfg(feature = "native")]
4746            BuiltinId::DataProfile => {
4747                if args.is_empty() {
4748                    return Err(runtime_err("data_profile() expects (table)"));
4749                }
4750                let df = match &args[0] {
4751                    VmValue::Table(t) => t.df.clone(),
4752                    _ => return Err(runtime_err("data_profile() arg must be a table")),
4753                };
4754                let result = self.engine().data_profile(df).map_err(runtime_err)?;
4755                Ok(VmValue::Table(VmTable { df: result }))
4756            }
4757            #[cfg(feature = "native")]
4758            BuiltinId::RowCount => {
4759                if args.is_empty() {
4760                    return Err(runtime_err("row_count() expects (table)"));
4761                }
4762                let df = match &args[0] {
4763                    VmValue::Table(t) => t.df.clone(),
4764                    _ => return Err(runtime_err("row_count() arg must be a table")),
4765                };
4766                let count = self.engine().row_count(df).map_err(runtime_err)?;
4767                Ok(VmValue::Int(count))
4768            }
4769            #[cfg(feature = "native")]
4770            BuiltinId::NullRate => {
4771                if args.len() < 2 {
4772                    return Err(runtime_err("null_rate() expects (table, column)"));
4773                }
4774                let df = match &args[0] {
4775                    VmValue::Table(t) => t.df.clone(),
4776                    _ => return Err(runtime_err("null_rate() first arg must be a table")),
4777                };
4778                let column = match &args[1] {
4779                    VmValue::String(s) => s.to_string(),
4780                    _ => return Err(runtime_err("null_rate() column must be a string")),
4781                };
4782                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
4783                Ok(VmValue::Float(rate))
4784            }
4785            #[cfg(feature = "native")]
4786            BuiltinId::IsUnique => {
4787                if args.len() < 2 {
4788                    return Err(runtime_err("is_unique() expects (table, column)"));
4789                }
4790                let df = match &args[0] {
4791                    VmValue::Table(t) => t.df.clone(),
4792                    _ => return Err(runtime_err("is_unique() first arg must be a table")),
4793                };
4794                let column = match &args[1] {
4795                    VmValue::String(s) => s.to_string(),
4796                    _ => return Err(runtime_err("is_unique() column must be a string")),
4797                };
4798                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
4799                Ok(VmValue::Bool(unique))
4800            }
4801            #[cfg(not(feature = "native"))]
4802            BuiltinId::FillNull
4803            | BuiltinId::DropNull
4804            | BuiltinId::Dedup
4805            | BuiltinId::Clamp
4806            | BuiltinId::DataProfile
4807            | BuiltinId::RowCount
4808            | BuiltinId::NullRate
4809            | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
4810            #[cfg(feature = "native")]
4811            BuiltinId::IsEmail => {
4812                if args.is_empty() {
4813                    return Err(runtime_err("is_email() expects 1 argument"));
4814                }
4815                let s = match &args[0] {
4816                    VmValue::String(s) => s.to_string(),
4817                    _ => return Err(runtime_err("is_email() arg must be a string")),
4818                };
4819                Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
4820            }
4821            #[cfg(feature = "native")]
4822            BuiltinId::IsUrl => {
4823                if args.is_empty() {
4824                    return Err(runtime_err("is_url() expects 1 argument"));
4825                }
4826                let s = match &args[0] {
4827                    VmValue::String(s) => s.to_string(),
4828                    _ => return Err(runtime_err("is_url() arg must be a string")),
4829                };
4830                Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
4831            }
4832            #[cfg(feature = "native")]
4833            BuiltinId::IsPhone => {
4834                if args.is_empty() {
4835                    return Err(runtime_err("is_phone() expects 1 argument"));
4836                }
4837                let s = match &args[0] {
4838                    VmValue::String(s) => s.to_string(),
4839                    _ => return Err(runtime_err("is_phone() arg must be a string")),
4840                };
4841                Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
4842            }
4843            #[cfg(feature = "native")]
4844            BuiltinId::IsBetween => {
4845                if args.len() < 3 {
4846                    return Err(runtime_err("is_between() expects (value, low, high)"));
4847                }
4848                let val = match &args[0] {
4849                    VmValue::Int(n) => *n as f64,
4850                    VmValue::Float(f) => *f,
4851                    _ => return Err(runtime_err("is_between() value must be a number")),
4852                };
4853                let low = match &args[1] {
4854                    VmValue::Int(n) => *n as f64,
4855                    VmValue::Float(f) => *f,
4856                    _ => return Err(runtime_err("is_between() low must be a number")),
4857                };
4858                let high = match &args[2] {
4859                    VmValue::Int(n) => *n as f64,
4860                    VmValue::Float(f) => *f,
4861                    _ => return Err(runtime_err("is_between() high must be a number")),
4862                };
4863                Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
4864            }
4865            #[cfg(feature = "native")]
4866            BuiltinId::Levenshtein => {
4867                if args.len() < 2 {
4868                    return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
4869                }
4870                let a = match &args[0] {
4871                    VmValue::String(s) => s.to_string(),
4872                    _ => return Err(runtime_err("levenshtein() args must be strings")),
4873                };
4874                let b = match &args[1] {
4875                    VmValue::String(s) => s.to_string(),
4876                    _ => return Err(runtime_err("levenshtein() args must be strings")),
4877                };
4878                Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
4879            }
4880            #[cfg(feature = "native")]
4881            BuiltinId::Soundex => {
4882                if args.is_empty() {
4883                    return Err(runtime_err("soundex() expects 1 argument"));
4884                }
4885                let s = match &args[0] {
4886                    VmValue::String(s) => s.to_string(),
4887                    _ => return Err(runtime_err("soundex() arg must be a string")),
4888                };
4889                Ok(VmValue::String(Arc::from(
4890                    tl_data::validate::soundex(&s).as_str(),
4891                )))
4892            }
4893            #[cfg(not(feature = "native"))]
4894            BuiltinId::IsEmail
4895            | BuiltinId::IsUrl
4896            | BuiltinId::IsPhone
4897            | BuiltinId::IsBetween
4898            | BuiltinId::Levenshtein
4899            | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
4900            #[cfg(feature = "native")]
4901            BuiltinId::ReadMysql => {
4902                #[cfg(feature = "mysql")]
4903                {
4904                    if args.len() < 2 {
4905                        return Err(runtime_err("read_mysql() expects (conn_str, query)"));
4906                    }
4907                    let conn_str = match &args[0] {
4908                        VmValue::String(s) => s.to_string(),
4909                        _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
4910                    };
4911                    let query = match &args[1] {
4912                        VmValue::String(s) => s.to_string(),
4913                        _ => return Err(runtime_err("read_mysql() query must be a string")),
4914                    };
4915                    let df = self
4916                        .engine()
4917                        .read_mysql(&conn_str, &query)
4918                        .map_err(runtime_err)?;
4919                    Ok(VmValue::Table(VmTable { df }))
4920                }
4921                #[cfg(not(feature = "mysql"))]
4922                Err(runtime_err("read_mysql() requires the 'mysql' feature"))
4923            }
4924            #[cfg(feature = "native")]
4925            BuiltinId::ReadSqlite => {
4926                #[cfg(feature = "sqlite")]
4927                {
4928                    if args.len() < 2 {
4929                        return Err(runtime_err("read_sqlite() expects (db_path, query)"));
4930                    }
4931                    let db_path = match &args[0] {
4932                        VmValue::String(s) => s.to_string(),
4933                        _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
4934                    };
4935                    let query = match &args[1] {
4936                        VmValue::String(s) => s.to_string(),
4937                        _ => return Err(runtime_err("read_sqlite() query must be a string")),
4938                    };
4939                    let df = self
4940                        .engine()
4941                        .read_sqlite(&db_path, &query)
4942                        .map_err(runtime_err)?;
4943                    Ok(VmValue::Table(VmTable { df }))
4944                }
4945                #[cfg(not(feature = "sqlite"))]
4946                Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
4947            }
4948            #[cfg(feature = "native")]
4949            BuiltinId::WriteSqlite => {
4950                #[cfg(feature = "sqlite")]
4951                {
4952                    if args.len() < 3 {
4953                        return Err(runtime_err(
4954                            "write_sqlite() expects (table, db_path, table_name)",
4955                        ));
4956                    }
4957                    let df = match &args[0] {
4958                        VmValue::Table(t) => t.df.clone(),
4959                        _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
4960                    };
4961                    let db_path = match &args[1] {
4962                        VmValue::String(s) => s.to_string(),
4963                        _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
4964                    };
4965                    let table_name = match &args[2] {
4966                        VmValue::String(s) => s.to_string(),
4967                        _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
4968                    };
4969                    self.engine()
4970                        .write_sqlite(df, &db_path, &table_name)
4971                        .map_err(runtime_err)?;
4972                    Ok(VmValue::None)
4973                }
4974                #[cfg(not(feature = "sqlite"))]
4975                Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
4976            }
4977            #[cfg(feature = "native")]
4978            BuiltinId::ReadDuckDb => {
4979                #[cfg(feature = "duckdb")]
4980                {
4981                    if args.len() < 2 {
4982                        return Err(runtime_err("duckdb() expects (db_path, query)"));
4983                    }
4984                    let db_path = match &args[0] {
4985                        VmValue::String(s) => s.to_string(),
4986                        _ => return Err(runtime_err("duckdb() db_path must be a string")),
4987                    };
4988                    let query = match &args[1] {
4989                        VmValue::String(s) => s.to_string(),
4990                        _ => return Err(runtime_err("duckdb() query must be a string")),
4991                    };
4992                    let df = self
4993                        .engine()
4994                        .read_duckdb(&db_path, &query)
4995                        .map_err(runtime_err)?;
4996                    Ok(VmValue::Table(VmTable { df }))
4997                }
4998                #[cfg(not(feature = "duckdb"))]
4999                Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5000            }
5001            #[cfg(feature = "native")]
5002            BuiltinId::WriteDuckDb => {
5003                #[cfg(feature = "duckdb")]
5004                {
5005                    if args.len() < 3 {
5006                        return Err(runtime_err(
5007                            "write_duckdb() expects (table, db_path, table_name)",
5008                        ));
5009                    }
5010                    let df = match &args[0] {
5011                        VmValue::Table(t) => t.df.clone(),
5012                        _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5013                    };
5014                    let db_path = match &args[1] {
5015                        VmValue::String(s) => s.to_string(),
5016                        _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5017                    };
5018                    let table_name = match &args[2] {
5019                        VmValue::String(s) => s.to_string(),
5020                        _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5021                    };
5022                    self.engine()
5023                        .write_duckdb(df, &db_path, &table_name)
5024                        .map_err(runtime_err)?;
5025                    Ok(VmValue::None)
5026                }
5027                #[cfg(not(feature = "duckdb"))]
5028                Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5029            }
5030            #[cfg(feature = "native")]
5031            BuiltinId::ReadRedshift => {
5032                if args.len() < 2 {
5033                    return Err(runtime_err("redshift() expects (conn_str, query)"));
5034                }
5035                let conn_str = match &args[0] {
5036                    VmValue::String(s) => {
5037                        let s_str = s.to_string();
5038                        resolve_tl_config_connection(&s_str)
5039                    }
5040                    _ => return Err(runtime_err("redshift() conn_str must be a string")),
5041                };
5042                let query = match &args[1] {
5043                    VmValue::String(s) => s.to_string(),
5044                    _ => return Err(runtime_err("redshift() query must be a string")),
5045                };
5046                let df = self
5047                    .engine()
5048                    .read_redshift(&conn_str, &query)
5049                    .map_err(runtime_err)?;
5050                Ok(VmValue::Table(VmTable { df }))
5051            }
5052            #[cfg(feature = "native")]
5053            BuiltinId::ReadMssql => {
5054                #[cfg(feature = "mssql")]
5055                {
5056                    if args.len() < 2 {
5057                        return Err(runtime_err("mssql() expects (conn_str, query)"));
5058                    }
5059                    let conn_str = match &args[0] {
5060                        VmValue::String(s) => {
5061                            let s_str = s.to_string();
5062                            resolve_tl_config_connection(&s_str)
5063                        }
5064                        _ => return Err(runtime_err("mssql() conn_str must be a string")),
5065                    };
5066                    let query = match &args[1] {
5067                        VmValue::String(s) => s.to_string(),
5068                        _ => return Err(runtime_err("mssql() query must be a string")),
5069                    };
5070                    let df = self
5071                        .engine()
5072                        .read_mssql(&conn_str, &query)
5073                        .map_err(runtime_err)?;
5074                    Ok(VmValue::Table(VmTable { df }))
5075                }
5076                #[cfg(not(feature = "mssql"))]
5077                Err(runtime_err("mssql() requires the 'mssql' feature"))
5078            }
5079            #[cfg(feature = "native")]
5080            BuiltinId::ReadSnowflake => {
5081                #[cfg(feature = "snowflake")]
5082                {
5083                    if args.len() < 2 {
5084                        return Err(runtime_err("snowflake() expects (config, query)"));
5085                    }
5086                    let config = match &args[0] {
5087                        VmValue::String(s) => {
5088                            let s_str = s.to_string();
5089                            resolve_tl_config_connection(&s_str)
5090                        }
5091                        _ => return Err(runtime_err("snowflake() config must be a string")),
5092                    };
5093                    let query = match &args[1] {
5094                        VmValue::String(s) => s.to_string(),
5095                        _ => return Err(runtime_err("snowflake() query must be a string")),
5096                    };
5097                    let df = self
5098                        .engine()
5099                        .read_snowflake(&config, &query)
5100                        .map_err(runtime_err)?;
5101                    Ok(VmValue::Table(VmTable { df }))
5102                }
5103                #[cfg(not(feature = "snowflake"))]
5104                Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5105            }
5106            #[cfg(feature = "native")]
5107            BuiltinId::ReadBigQuery => {
5108                #[cfg(feature = "bigquery")]
5109                {
5110                    if args.len() < 2 {
5111                        return Err(runtime_err("bigquery() expects (config, query)"));
5112                    }
5113                    let config = match &args[0] {
5114                        VmValue::String(s) => {
5115                            let s_str = s.to_string();
5116                            resolve_tl_config_connection(&s_str)
5117                        }
5118                        _ => return Err(runtime_err("bigquery() config must be a string")),
5119                    };
5120                    let query = match &args[1] {
5121                        VmValue::String(s) => s.to_string(),
5122                        _ => return Err(runtime_err("bigquery() query must be a string")),
5123                    };
5124                    let df = self
5125                        .engine()
5126                        .read_bigquery(&config, &query)
5127                        .map_err(runtime_err)?;
5128                    Ok(VmValue::Table(VmTable { df }))
5129                }
5130                #[cfg(not(feature = "bigquery"))]
5131                Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5132            }
5133            #[cfg(feature = "native")]
5134            BuiltinId::ReadDatabricks => {
5135                #[cfg(feature = "databricks")]
5136                {
5137                    if args.len() < 2 {
5138                        return Err(runtime_err("databricks() expects (config, query)"));
5139                    }
5140                    let config = match &args[0] {
5141                        VmValue::String(s) => {
5142                            let s_str = s.to_string();
5143                            resolve_tl_config_connection(&s_str)
5144                        }
5145                        _ => return Err(runtime_err("databricks() config must be a string")),
5146                    };
5147                    let query = match &args[1] {
5148                        VmValue::String(s) => s.to_string(),
5149                        _ => return Err(runtime_err("databricks() query must be a string")),
5150                    };
5151                    let df = self
5152                        .engine()
5153                        .read_databricks(&config, &query)
5154                        .map_err(runtime_err)?;
5155                    Ok(VmValue::Table(VmTable { df }))
5156                }
5157                #[cfg(not(feature = "databricks"))]
5158                Err(runtime_err(
5159                    "databricks() requires the 'databricks' feature",
5160                ))
5161            }
5162            #[cfg(feature = "native")]
5163            BuiltinId::ReadClickHouse => {
5164                #[cfg(feature = "clickhouse")]
5165                {
5166                    if args.len() < 2 {
5167                        return Err(runtime_err("clickhouse() expects (url, query)"));
5168                    }
5169                    let url = match &args[0] {
5170                        VmValue::String(s) => {
5171                            let s_str = s.to_string();
5172                            resolve_tl_config_connection(&s_str)
5173                        }
5174                        _ => return Err(runtime_err("clickhouse() url must be a string")),
5175                    };
5176                    let query = match &args[1] {
5177                        VmValue::String(s) => s.to_string(),
5178                        _ => return Err(runtime_err("clickhouse() query must be a string")),
5179                    };
5180                    let df = self
5181                        .engine()
5182                        .read_clickhouse(&url, &query)
5183                        .map_err(runtime_err)?;
5184                    Ok(VmValue::Table(VmTable { df }))
5185                }
5186                #[cfg(not(feature = "clickhouse"))]
5187                Err(runtime_err(
5188                    "clickhouse() requires the 'clickhouse' feature",
5189                ))
5190            }
5191            #[cfg(feature = "native")]
5192            BuiltinId::ReadMongo => {
5193                #[cfg(feature = "mongodb")]
5194                {
5195                    if args.len() < 4 {
5196                        return Err(runtime_err(
5197                            "mongo() expects (conn_str, database, collection, filter_json)",
5198                        ));
5199                    }
5200                    let conn_str = match &args[0] {
5201                        VmValue::String(s) => {
5202                            let s_str = s.to_string();
5203                            resolve_tl_config_connection(&s_str)
5204                        }
5205                        _ => return Err(runtime_err("mongo() conn_str must be a string")),
5206                    };
5207                    let database = match &args[1] {
5208                        VmValue::String(s) => s.to_string(),
5209                        _ => return Err(runtime_err("mongo() database must be a string")),
5210                    };
5211                    let collection = match &args[2] {
5212                        VmValue::String(s) => s.to_string(),
5213                        _ => return Err(runtime_err("mongo() collection must be a string")),
5214                    };
5215                    let filter_json = match &args[3] {
5216                        VmValue::String(s) => s.to_string(),
5217                        _ => return Err(runtime_err("mongo() filter must be a string")),
5218                    };
5219                    let df = self
5220                        .engine()
5221                        .read_mongo(&conn_str, &database, &collection, &filter_json)
5222                        .map_err(runtime_err)?;
5223                    Ok(VmValue::Table(VmTable { df }))
5224                }
5225                #[cfg(not(feature = "mongodb"))]
5226                Err(runtime_err("mongo() requires the 'mongodb' feature"))
5227            }
5228            #[cfg(feature = "native")]
5229            BuiltinId::SftpDownload => {
5230                #[cfg(feature = "sftp")]
5231                {
5232                    if args.len() < 3 {
5233                        return Err(runtime_err(
5234                            "sftp_download() expects (config, remote_path, local_path)",
5235                        ));
5236                    }
5237                    let config = match &args[0] {
5238                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5239                        _ => return Err(runtime_err("sftp_download() config must be a string")),
5240                    };
5241                    let remote = match &args[1] {
5242                        VmValue::String(s) => s.to_string(),
5243                        _ => {
5244                            return Err(runtime_err(
5245                                "sftp_download() remote_path must be a string",
5246                            ));
5247                        }
5248                    };
5249                    let local = match &args[2] {
5250                        VmValue::String(s) => s.to_string(),
5251                        _ => {
5252                            return Err(runtime_err("sftp_download() local_path must be a string"));
5253                        }
5254                    };
5255                    let result = self
5256                        .engine()
5257                        .sftp_download(&config, &remote, &local)
5258                        .map_err(runtime_err)?;
5259                    Ok(VmValue::String(Arc::from(result.as_str())))
5260                }
5261                #[cfg(not(feature = "sftp"))]
5262                Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5263            }
5264            #[cfg(feature = "native")]
5265            BuiltinId::SftpUpload => {
5266                #[cfg(feature = "sftp")]
5267                {
5268                    if args.len() < 3 {
5269                        return Err(runtime_err(
5270                            "sftp_upload() expects (config, local_path, remote_path)",
5271                        ));
5272                    }
5273                    let config = match &args[0] {
5274                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5275                        _ => return Err(runtime_err("sftp_upload() config must be a string")),
5276                    };
5277                    let local = match &args[1] {
5278                        VmValue::String(s) => s.to_string(),
5279                        _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5280                    };
5281                    let remote = match &args[2] {
5282                        VmValue::String(s) => s.to_string(),
5283                        _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5284                    };
5285                    let result = self
5286                        .engine()
5287                        .sftp_upload(&config, &local, &remote)
5288                        .map_err(runtime_err)?;
5289                    Ok(VmValue::String(Arc::from(result.as_str())))
5290                }
5291                #[cfg(not(feature = "sftp"))]
5292                Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5293            }
5294            #[cfg(feature = "native")]
5295            BuiltinId::SftpList => {
5296                #[cfg(feature = "sftp")]
5297                {
5298                    if args.len() < 2 {
5299                        return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5300                    }
5301                    let config = match &args[0] {
5302                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5303                        _ => return Err(runtime_err("sftp_list() config must be a string")),
5304                    };
5305                    let remote = match &args[1] {
5306                        VmValue::String(s) => s.to_string(),
5307                        _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5308                    };
5309                    let df = self
5310                        .engine()
5311                        .sftp_list(&config, &remote)
5312                        .map_err(runtime_err)?;
5313                    Ok(VmValue::Table(VmTable { df }))
5314                }
5315                #[cfg(not(feature = "sftp"))]
5316                Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5317            }
5318            #[cfg(feature = "native")]
5319            BuiltinId::SftpReadCsv => {
5320                #[cfg(feature = "sftp")]
5321                {
5322                    if args.len() < 2 {
5323                        return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5324                    }
5325                    let config = match &args[0] {
5326                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5327                        _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5328                    };
5329                    let remote = match &args[1] {
5330                        VmValue::String(s) => s.to_string(),
5331                        _ => {
5332                            return Err(runtime_err(
5333                                "sftp_read_csv() remote_path must be a string",
5334                            ));
5335                        }
5336                    };
5337                    let df = self
5338                        .engine()
5339                        .sftp_read_csv(&config, &remote)
5340                        .map_err(runtime_err)?;
5341                    Ok(VmValue::Table(VmTable { df }))
5342                }
5343                #[cfg(not(feature = "sftp"))]
5344                Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5345            }
5346            #[cfg(feature = "native")]
5347            BuiltinId::SftpReadParquet => {
5348                #[cfg(feature = "sftp")]
5349                {
5350                    if args.len() < 2 {
5351                        return Err(runtime_err(
5352                            "sftp_read_parquet() expects (config, remote_path)",
5353                        ));
5354                    }
5355                    let config = match &args[0] {
5356                        VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5357                        _ => {
5358                            return Err(runtime_err("sftp_read_parquet() config must be a string"));
5359                        }
5360                    };
5361                    let remote = match &args[1] {
5362                        VmValue::String(s) => s.to_string(),
5363                        _ => {
5364                            return Err(runtime_err(
5365                                "sftp_read_parquet() remote_path must be a string",
5366                            ));
5367                        }
5368                    };
5369                    let df = self
5370                        .engine()
5371                        .sftp_read_parquet(&config, &remote)
5372                        .map_err(runtime_err)?;
5373                    Ok(VmValue::Table(VmTable { df }))
5374                }
5375                #[cfg(not(feature = "sftp"))]
5376                Err(runtime_err(
5377                    "sftp_read_parquet() requires the 'sftp' feature",
5378                ))
5379            }
5380            #[cfg(feature = "native")]
5381            BuiltinId::RedisConnect => {
5382                #[cfg(feature = "redis")]
5383                {
5384                    if args.is_empty() {
5385                        return Err(runtime_err("redis_connect() expects (url)"));
5386                    }
5387                    let url = match &args[0] {
5388                        VmValue::String(s) => s.to_string(),
5389                        _ => return Err(runtime_err("redis_connect() url must be a string")),
5390                    };
5391                    let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5392                    Ok(VmValue::String(Arc::from(result.as_str())))
5393                }
5394                #[cfg(not(feature = "redis"))]
5395                Err(runtime_err("redis_connect() requires the 'redis' feature"))
5396            }
5397            #[cfg(feature = "native")]
5398            BuiltinId::RedisGet => {
5399                #[cfg(feature = "redis")]
5400                {
5401                    if args.len() < 2 {
5402                        return Err(runtime_err("redis_get() expects (url, key)"));
5403                    }
5404                    let url = match &args[0] {
5405                        VmValue::String(s) => s.to_string(),
5406                        _ => return Err(runtime_err("redis_get() url must be a string")),
5407                    };
5408                    let key = match &args[1] {
5409                        VmValue::String(s) => s.to_string(),
5410                        _ => return Err(runtime_err("redis_get() key must be a string")),
5411                    };
5412                    match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5413                        Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5414                        None => Ok(VmValue::None),
5415                    }
5416                }
5417                #[cfg(not(feature = "redis"))]
5418                Err(runtime_err("redis_get() requires the 'redis' feature"))
5419            }
5420            #[cfg(feature = "native")]
5421            BuiltinId::RedisSet => {
5422                #[cfg(feature = "redis")]
5423                {
5424                    if args.len() < 3 {
5425                        return Err(runtime_err("redis_set() expects (url, key, value)"));
5426                    }
5427                    let url = match &args[0] {
5428                        VmValue::String(s) => s.to_string(),
5429                        _ => return Err(runtime_err("redis_set() url must be a string")),
5430                    };
5431                    let key = match &args[1] {
5432                        VmValue::String(s) => s.to_string(),
5433                        _ => return Err(runtime_err("redis_set() key must be a string")),
5434                    };
5435                    let value = match &args[2] {
5436                        VmValue::String(s) => s.to_string(),
5437                        _ => format!("{}", &args[2]),
5438                    };
5439                    tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5440                    Ok(VmValue::None)
5441                }
5442                #[cfg(not(feature = "redis"))]
5443                Err(runtime_err("redis_set() requires the 'redis' feature"))
5444            }
5445            #[cfg(feature = "native")]
5446            BuiltinId::RedisDel => {
5447                #[cfg(feature = "redis")]
5448                {
5449                    if args.len() < 2 {
5450                        return Err(runtime_err("redis_del() expects (url, key)"));
5451                    }
5452                    let url = match &args[0] {
5453                        VmValue::String(s) => s.to_string(),
5454                        _ => return Err(runtime_err("redis_del() url must be a string")),
5455                    };
5456                    let key = match &args[1] {
5457                        VmValue::String(s) => s.to_string(),
5458                        _ => return Err(runtime_err("redis_del() key must be a string")),
5459                    };
5460                    let deleted =
5461                        tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5462                    Ok(VmValue::Bool(deleted))
5463                }
5464                #[cfg(not(feature = "redis"))]
5465                Err(runtime_err("redis_del() requires the 'redis' feature"))
5466            }
5467            #[cfg(feature = "native")]
5468            BuiltinId::GraphqlQuery => {
5469                if args.len() < 2 {
5470                    return Err(runtime_err(
5471                        "graphql_query() expects (endpoint, query, [variables])",
5472                    ));
5473                }
5474                let endpoint = match &args[0] {
5475                    VmValue::String(s) => s.to_string(),
5476                    _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5477                };
5478                let query = match &args[1] {
5479                    VmValue::String(s) => s.to_string(),
5480                    _ => return Err(runtime_err("graphql_query() query must be a string")),
5481                };
5482                let variables = if args.len() > 2 {
5483                    vm_value_to_json(&args[2])
5484                } else {
5485                    serde_json::Value::Null
5486                };
5487                let mut body = serde_json::Map::new();
5488                body.insert("query".to_string(), serde_json::Value::String(query));
5489                if !variables.is_null() {
5490                    body.insert("variables".to_string(), variables);
5491                }
5492                let client = reqwest::blocking::Client::new();
5493                let resp = client
5494                    .post(&endpoint)
5495                    .header("Content-Type", "application/json")
5496                    .json(&body)
5497                    .send()
5498                    .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5499                let text = resp
5500                    .text()
5501                    .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5502                let json: serde_json::Value = serde_json::from_str(&text)
5503                    .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5504                Ok(vm_json_to_value(&json))
5505            }
5506            #[cfg(feature = "native")]
5507            BuiltinId::RegisterS3 => {
5508                #[cfg(feature = "s3")]
5509                {
5510                    if args.len() < 2 {
5511                        return Err(runtime_err(
5512                            "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5513                        ));
5514                    }
5515                    let bucket = match &args[0] {
5516                        VmValue::String(s) => s.to_string(),
5517                        _ => return Err(runtime_err("register_s3() bucket must be a string")),
5518                    };
5519                    let region = match &args[1] {
5520                        VmValue::String(s) => s.to_string(),
5521                        _ => return Err(runtime_err("register_s3() region must be a string")),
5522                    };
5523                    let access_key = args.get(2).and_then(|v| {
5524                        if let VmValue::String(s) = v {
5525                            Some(s.to_string())
5526                        } else {
5527                            None
5528                        }
5529                    });
5530                    let secret_key = args.get(3).and_then(|v| {
5531                        if let VmValue::String(s) = v {
5532                            Some(s.to_string())
5533                        } else {
5534                            None
5535                        }
5536                    });
5537                    let endpoint = args.get(4).and_then(|v| {
5538                        if let VmValue::String(s) = v {
5539                            Some(s.to_string())
5540                        } else {
5541                            None
5542                        }
5543                    });
5544                    self.engine()
5545                        .register_s3(
5546                            &bucket,
5547                            &region,
5548                            access_key.as_deref(),
5549                            secret_key.as_deref(),
5550                            endpoint.as_deref(),
5551                        )
5552                        .map_err(runtime_err)?;
5553                    Ok(VmValue::None)
5554                }
5555                #[cfg(not(feature = "s3"))]
5556                Err(runtime_err("register_s3() requires the 's3' feature"))
5557            }
5558            #[cfg(not(feature = "native"))]
5559            BuiltinId::ReadMysql
5560            | BuiltinId::ReadSqlite
5561            | BuiltinId::WriteSqlite
5562            | BuiltinId::ReadDuckDb
5563            | BuiltinId::WriteDuckDb
5564            | BuiltinId::ReadRedshift
5565            | BuiltinId::ReadMssql
5566            | BuiltinId::ReadSnowflake
5567            | BuiltinId::ReadBigQuery
5568            | BuiltinId::ReadDatabricks
5569            | BuiltinId::ReadClickHouse
5570            | BuiltinId::ReadMongo
5571            | BuiltinId::SftpDownload
5572            | BuiltinId::SftpUpload
5573            | BuiltinId::SftpList
5574            | BuiltinId::SftpReadCsv
5575            | BuiltinId::SftpReadParquet
5576            | BuiltinId::RedisConnect
5577            | BuiltinId::RedisGet
5578            | BuiltinId::RedisSet
5579            | BuiltinId::RedisDel
5580            | BuiltinId::GraphqlQuery
5581            | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
5582            // Phase 20: Python FFI
5583            BuiltinId::PyImport => {
5584                self.check_permission("python")?;
5585                #[cfg(feature = "python")]
5586                {
5587                    crate::python::py_import_impl(&args)
5588                }
5589                #[cfg(not(feature = "python"))]
5590                Err(runtime_err("py_import() requires the 'python' feature"))
5591            }
5592            BuiltinId::PyCall => {
5593                self.check_permission("python")?;
5594                #[cfg(feature = "python")]
5595                {
5596                    crate::python::py_call_impl(&args)
5597                }
5598                #[cfg(not(feature = "python"))]
5599                Err(runtime_err("py_call() requires the 'python' feature"))
5600            }
5601            BuiltinId::PyEval => {
5602                self.check_permission("python")?;
5603                #[cfg(feature = "python")]
5604                {
5605                    crate::python::py_eval_impl(&args)
5606                }
5607                #[cfg(not(feature = "python"))]
5608                Err(runtime_err("py_eval() requires the 'python' feature"))
5609            }
5610            BuiltinId::PyGetAttr => {
5611                self.check_permission("python")?;
5612                #[cfg(feature = "python")]
5613                {
5614                    crate::python::py_getattr_impl(&args)
5615                }
5616                #[cfg(not(feature = "python"))]
5617                Err(runtime_err("py_getattr() requires the 'python' feature"))
5618            }
5619            BuiltinId::PySetAttr => {
5620                self.check_permission("python")?;
5621                #[cfg(feature = "python")]
5622                {
5623                    crate::python::py_setattr_impl(&args)
5624                }
5625                #[cfg(not(feature = "python"))]
5626                Err(runtime_err("py_setattr() requires the 'python' feature"))
5627            }
5628            BuiltinId::PyToTl => {
5629                #[cfg(feature = "python")]
5630                {
5631                    crate::python::py_to_tl_impl(&args)
5632                }
5633                #[cfg(not(feature = "python"))]
5634                Err(runtime_err("py_to_tl() requires the 'python' feature"))
5635            }
5636
5637            // Phase 21: Schema Evolution builtins
5638            #[cfg(feature = "native")]
5639            BuiltinId::SchemaRegister => {
5640                let name = match args.first() {
5641                    Some(VmValue::String(s)) => s.to_string(),
5642                    _ => {
5643                        return Err(runtime_err(
5644                            "schema_register: first arg must be schema name string",
5645                        ));
5646                    }
5647                };
5648                let version = match args.get(1) {
5649                    Some(VmValue::Int(v)) => *v,
5650                    _ => {
5651                        return Err(runtime_err(
5652                            "schema_register: second arg must be version number",
5653                        ));
5654                    }
5655                };
5656                let fields = match args.get(2) {
5657                    Some(VmValue::Map(pairs)) => {
5658                        let mut arrow_fields = Vec::new();
5659                        for (k, v) in pairs.iter() {
5660                            let fname = k.to_string();
5661                            let ftype = match v {
5662                                VmValue::String(s) => s.to_string(),
5663                                _ => "string".to_string(),
5664                            };
5665                            arrow_fields.push(tl_data::ArrowField::new(
5666                                &fname,
5667                                crate::schema::type_name_to_arrow_pub(&ftype),
5668                                true,
5669                            ));
5670                        }
5671                        arrow_fields
5672                    }
5673                    _ => return Err(runtime_err("schema_register: third arg must be fields map")),
5674                };
5675                let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
5676                self.schema_registry
5677                    .register(
5678                        &name,
5679                        version,
5680                        schema,
5681                        crate::schema::SchemaMetadata::default(),
5682                    )
5683                    .map_err(|e| runtime_err(&e))?;
5684                Ok(VmValue::None)
5685            }
5686            #[cfg(feature = "native")]
5687            BuiltinId::SchemaGet => {
5688                let name = match args.first() {
5689                    Some(VmValue::String(s)) => s.to_string(),
5690                    _ => return Err(runtime_err("schema_get: need name")),
5691                };
5692                let version = match args.get(1) {
5693                    Some(VmValue::Int(v)) => *v,
5694                    _ => return Err(runtime_err("schema_get: need version")),
5695                };
5696                match self.schema_registry.get(&name, version) {
5697                    Some(vs) => {
5698                        let fields: Vec<VmValue> = vs
5699                            .schema
5700                            .fields()
5701                            .iter()
5702                            .map(|f| {
5703                                VmValue::String(std::sync::Arc::from(format!(
5704                                    "{}: {}",
5705                                    f.name(),
5706                                    f.data_type()
5707                                )))
5708                            })
5709                            .collect();
5710                        Ok(VmValue::List(Box::new(fields)))
5711                    }
5712                    None => Ok(VmValue::None),
5713                }
5714            }
5715            #[cfg(feature = "native")]
5716            BuiltinId::SchemaLatest => {
5717                let name = match args.first() {
5718                    Some(VmValue::String(s)) => s.to_string(),
5719                    _ => return Err(runtime_err("schema_latest: need name")),
5720                };
5721                match self.schema_registry.latest(&name) {
5722                    Some(vs) => Ok(VmValue::Int(vs.version)),
5723                    None => Ok(VmValue::None),
5724                }
5725            }
5726            #[cfg(feature = "native")]
5727            BuiltinId::SchemaHistory => {
5728                let name = match args.first() {
5729                    Some(VmValue::String(s)) => s.to_string(),
5730                    _ => return Err(runtime_err("schema_history: need name")),
5731                };
5732                let versions = self.schema_registry.versions(&name);
5733                Ok(VmValue::List(Box::new(
5734                    versions.into_iter().map(VmValue::Int).collect(),
5735                )))
5736            }
5737            #[cfg(feature = "native")]
5738            BuiltinId::SchemaCheck => {
5739                let name = match args.first() {
5740                    Some(VmValue::String(s)) => s.to_string(),
5741                    _ => return Err(runtime_err("schema_check: need name")),
5742                };
5743                let v1 = match args.get(1) {
5744                    Some(VmValue::Int(v)) => *v,
5745                    _ => return Err(runtime_err("schema_check: need v1")),
5746                };
5747                let v2 = match args.get(2) {
5748                    Some(VmValue::Int(v)) => *v,
5749                    _ => return Err(runtime_err("schema_check: need v2")),
5750                };
5751                let mode_str = match args.get(3) {
5752                    Some(VmValue::String(s)) => s.to_string(),
5753                    _ => "backward".to_string(),
5754                };
5755                let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
5756                let issues = self
5757                    .schema_registry
5758                    .check_compatibility(&name, v1, v2, mode);
5759                Ok(VmValue::List(Box::new(
5760                    issues
5761                        .into_iter()
5762                        .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
5763                        .collect(),
5764                )))
5765            }
5766            #[cfg(feature = "native")]
5767            BuiltinId::SchemaDiff => {
5768                let name = match args.first() {
5769                    Some(VmValue::String(s)) => s.to_string(),
5770                    _ => return Err(runtime_err("schema_diff: need name")),
5771                };
5772                let v1 = match args.get(1) {
5773                    Some(VmValue::Int(v)) => *v,
5774                    _ => return Err(runtime_err("schema_diff: need v1")),
5775                };
5776                let v2 = match args.get(2) {
5777                    Some(VmValue::Int(v)) => *v,
5778                    _ => return Err(runtime_err("schema_diff: need v2")),
5779                };
5780                let diffs = self.schema_registry.diff(&name, v1, v2);
5781                Ok(VmValue::List(Box::new(
5782                    diffs
5783                        .into_iter()
5784                        .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
5785                        .collect(),
5786                )))
5787            }
5788            #[cfg(feature = "native")]
5789            BuiltinId::SchemaApplyMigration => {
5790                let name = match args.first() {
5791                    Some(VmValue::String(s)) => s.to_string(),
5792                    _ => return Err(runtime_err("schema_apply_migration: need name")),
5793                };
5794                let from_v = match args.get(1) {
5795                    Some(VmValue::Int(v)) => *v,
5796                    _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
5797                };
5798                let to_v = match args.get(2) {
5799                    Some(VmValue::Int(v)) => *v,
5800                    _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
5801                };
5802                Ok(VmValue::String(std::sync::Arc::from(format!(
5803                    "migration {}:{}->{} applied",
5804                    name, from_v, to_v
5805                ))))
5806            }
5807            #[cfg(feature = "native")]
5808            BuiltinId::SchemaVersions => {
5809                let name = match args.first() {
5810                    Some(VmValue::String(s)) => s.to_string(),
5811                    _ => return Err(runtime_err("schema_versions: need name")),
5812                };
5813                let versions = self.schema_registry.versions(&name);
5814                Ok(VmValue::List(Box::new(
5815                    versions.into_iter().map(VmValue::Int).collect(),
5816                )))
5817            }
5818            #[cfg(feature = "native")]
5819            BuiltinId::SchemaFields => {
5820                let name = match args.first() {
5821                    Some(VmValue::String(s)) => s.to_string(),
5822                    _ => return Err(runtime_err("schema_fields: need name")),
5823                };
5824                let version = match args.get(1) {
5825                    Some(VmValue::Int(v)) => *v,
5826                    _ => return Err(runtime_err("schema_fields: need version")),
5827                };
5828                let fields = self.schema_registry.fields(&name, version);
5829                Ok(VmValue::List(Box::new(
5830                    fields
5831                        .into_iter()
5832                        .map(|(n, t)| {
5833                            VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
5834                        })
5835                        .collect(),
5836                )))
5837            }
5838            #[cfg(not(feature = "native"))]
5839            BuiltinId::SchemaRegister
5840            | BuiltinId::SchemaGet
5841            | BuiltinId::SchemaLatest
5842            | BuiltinId::SchemaHistory
5843            | BuiltinId::SchemaCheck
5844            | BuiltinId::SchemaDiff
5845            | BuiltinId::SchemaApplyMigration
5846            | BuiltinId::SchemaVersions
5847            | BuiltinId::SchemaFields => {
5848                let _ = args;
5849                Err(runtime_err("Schema operations not available in WASM"))
5850            }
5851
5852            // ── Phase 22: Advanced Types ──
5853            BuiltinId::Decimal => {
5854                use std::str::FromStr;
5855                let s = match args.first() {
5856                    Some(VmValue::String(s)) => s.to_string(),
5857                    Some(VmValue::Int(n)) => n.to_string(),
5858                    Some(VmValue::Float(f)) => f.to_string(),
5859                    _ => return Err(runtime_err("decimal(): expected string, int, or float")),
5860                };
5861                let d = rust_decimal::Decimal::from_str(&s)
5862                    .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
5863                Ok(VmValue::Decimal(d))
5864            }
5865
5866            // ── Phase 23: Security ──
5867            BuiltinId::SecretGet => {
5868                let key = match args.first() {
5869                    Some(VmValue::String(s)) => s.to_string(),
5870                    _ => return Err(runtime_err("secret_get: need key")),
5871                };
5872                if let Some(val) = self.secret_vault.get(&key) {
5873                    Ok(VmValue::Secret(Arc::from(val.as_str())))
5874                } else {
5875                    // Fallback to env var TL_SECRET_{KEY}
5876                    let env_key = format!("TL_SECRET_{}", key.to_uppercase());
5877                    match std::env::var(&env_key) {
5878                        Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
5879                        Err(_) => Ok(VmValue::None),
5880                    }
5881                }
5882            }
5883            BuiltinId::SecretSet => {
5884                let key = match args.first() {
5885                    Some(VmValue::String(s)) => s.to_string(),
5886                    _ => return Err(runtime_err("secret_set: need key")),
5887                };
5888                let val = match args.get(1) {
5889                    Some(VmValue::String(s)) => s.to_string(),
5890                    Some(VmValue::Secret(s)) => s.to_string(),
5891                    _ => return Err(runtime_err("secret_set: need value")),
5892                };
5893                self.secret_vault.insert(key, val);
5894                Ok(VmValue::None)
5895            }
5896            BuiltinId::SecretDelete => {
5897                let key = match args.first() {
5898                    Some(VmValue::String(s)) => s.to_string(),
5899                    _ => return Err(runtime_err("secret_delete: need key")),
5900                };
5901                self.secret_vault.remove(&key);
5902                Ok(VmValue::None)
5903            }
5904            BuiltinId::SecretList => {
5905                let keys: Vec<VmValue> = self
5906                    .secret_vault
5907                    .keys()
5908                    .map(|k| VmValue::String(Arc::from(k.as_str())))
5909                    .collect();
5910                Ok(VmValue::List(Box::new(keys)))
5911            }
5912            BuiltinId::CheckPermission => {
5913                let perm = match args.first() {
5914                    Some(VmValue::String(s)) => s.to_string(),
5915                    _ => return Err(runtime_err("check_permission: need permission name")),
5916                };
5917                let allowed = match self.security_policy {
5918                    Some(ref policy) => policy.check(&perm),
5919                    None => true,
5920                };
5921                Ok(VmValue::Bool(allowed))
5922            }
5923            BuiltinId::MaskEmail => {
5924                let email = match args.first() {
5925                    Some(VmValue::String(s)) => s.to_string(),
5926                    _ => return Err(runtime_err("mask_email: need string")),
5927                };
5928                let masked = if let Some(at_pos) = email.find('@') {
5929                    let local = &email[..at_pos];
5930                    let domain = &email[at_pos..];
5931                    if local.len() > 1 {
5932                        format!("{}***{}", &local[..1], domain)
5933                    } else {
5934                        format!("***{domain}")
5935                    }
5936                } else {
5937                    "***".to_string()
5938                };
5939                Ok(VmValue::String(Arc::from(masked.as_str())))
5940            }
5941            BuiltinId::MaskPhone => {
5942                let phone = match args.first() {
5943                    Some(VmValue::String(s)) => s.to_string(),
5944                    _ => return Err(runtime_err("mask_phone: need string")),
5945                };
5946                let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
5947                let masked = if digits.len() >= 4 {
5948                    let last4 = &digits[digits.len() - 4..];
5949                    format!("***-***-{last4}")
5950                } else {
5951                    "***".to_string()
5952                };
5953                Ok(VmValue::String(Arc::from(masked.as_str())))
5954            }
5955            BuiltinId::MaskCreditCard => {
5956                let cc = match args.first() {
5957                    Some(VmValue::String(s)) => s.to_string(),
5958                    _ => return Err(runtime_err("mask_cc: need string")),
5959                };
5960                let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
5961                let masked = if digits.len() >= 4 {
5962                    let last4 = &digits[digits.len() - 4..];
5963                    format!("****-****-****-{last4}")
5964                } else {
5965                    "****-****-****-****".to_string()
5966                };
5967                Ok(VmValue::String(Arc::from(masked.as_str())))
5968            }
5969            BuiltinId::Redact => {
5970                let val = match args.first() {
5971                    Some(v) => format!("{v}"),
5972                    _ => return Err(runtime_err("redact: need value")),
5973                };
5974                let policy = match args.get(1) {
5975                    Some(VmValue::String(s)) => s.to_string(),
5976                    _ => "full".to_string(),
5977                };
5978                let result = match policy.as_str() {
5979                    "full" => "***".to_string(),
5980                    "partial" => {
5981                        if val.len() > 2 {
5982                            format!("{}***{}", &val[..1], &val[val.len() - 1..])
5983                        } else {
5984                            "***".to_string()
5985                        }
5986                    }
5987                    "hash" => {
5988                        use sha2::Digest;
5989                        let hash = sha2::Sha256::digest(val.as_bytes());
5990                        format!("{:x}", hash)
5991                    }
5992                    _ => "***".to_string(),
5993                };
5994                Ok(VmValue::String(Arc::from(result.as_str())))
5995            }
5996            BuiltinId::Hash => {
5997                let val = match args.first() {
5998                    Some(VmValue::String(s)) => s.to_string(),
5999                    _ => return Err(runtime_err("hash: need string")),
6000                };
6001                let algo = match args.get(1) {
6002                    Some(VmValue::String(s)) => s.to_string(),
6003                    _ => "sha256".to_string(),
6004                };
6005                let result = match algo.as_str() {
6006                    "sha256" => {
6007                        use sha2::Digest;
6008                        format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6009                    }
6010                    "sha512" => {
6011                        use sha2::Digest;
6012                        format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6013                    }
6014                    "md5" => {
6015                        use md5::Digest;
6016                        format!("{:x}", md5::Md5::digest(val.as_bytes()))
6017                    }
6018                    _ => {
6019                        return Err(runtime_err(format!(
6020                            "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6021                        )));
6022                    }
6023                };
6024                Ok(VmValue::String(Arc::from(result.as_str())))
6025            }
6026
6027            // ── Phase 25: Async builtins (tokio-backed when async-runtime feature enabled) ──
6028            #[cfg(feature = "async-runtime")]
6029            BuiltinId::AsyncReadFile => {
6030                let rt = self.ensure_runtime();
6031                crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6032            }
6033            #[cfg(feature = "async-runtime")]
6034            BuiltinId::AsyncWriteFile => {
6035                let rt = self.ensure_runtime();
6036                crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6037            }
6038            #[cfg(feature = "async-runtime")]
6039            BuiltinId::AsyncHttpGet => {
6040                let rt = self.ensure_runtime();
6041                crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6042            }
6043            #[cfg(feature = "async-runtime")]
6044            BuiltinId::AsyncHttpPost => {
6045                let rt = self.ensure_runtime();
6046                crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6047            }
6048            #[cfg(feature = "async-runtime")]
6049            BuiltinId::AsyncSleep => {
6050                let rt = self.ensure_runtime();
6051                crate::async_runtime::async_sleep_impl(&rt, &args)
6052            }
6053            #[cfg(feature = "async-runtime")]
6054            BuiltinId::Select => crate::async_runtime::select_impl(&args),
6055            #[cfg(feature = "async-runtime")]
6056            BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6057            #[cfg(feature = "async-runtime")]
6058            BuiltinId::AsyncMap => {
6059                let rt = self.ensure_runtime();
6060                let stack_snapshot = self.stack.clone();
6061                crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6062            }
6063            #[cfg(feature = "async-runtime")]
6064            BuiltinId::AsyncFilter => {
6065                let rt = self.ensure_runtime();
6066                let stack_snapshot = self.stack.clone();
6067                crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6068            }
6069
6070            #[cfg(not(feature = "async-runtime"))]
6071            BuiltinId::AsyncReadFile
6072            | BuiltinId::AsyncWriteFile
6073            | BuiltinId::AsyncHttpGet
6074            | BuiltinId::AsyncHttpPost
6075            | BuiltinId::AsyncSleep
6076            | BuiltinId::Select
6077            | BuiltinId::AsyncMap
6078            | BuiltinId::AsyncFilter
6079            | BuiltinId::RaceAll => Err(runtime_err(format!(
6080                "{}: async builtins require the 'async-runtime' feature",
6081                builtin_id.name()
6082            ))),
6083
6084            // Phase 27: Data Error Hierarchy builtins
6085            BuiltinId::IsError => {
6086                if args.is_empty() {
6087                    return Err(runtime_err("is_error() expects 1 argument"));
6088                }
6089                let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6090                    &*e.type_name == "DataError" ||
6091                    &*e.type_name == "NetworkError" ||
6092                    &*e.type_name == "ConnectorError"
6093                );
6094                Ok(VmValue::Bool(is_err))
6095            }
6096            BuiltinId::ErrorType => {
6097                if args.is_empty() {
6098                    return Err(runtime_err("error_type() expects 1 argument"));
6099                }
6100                match &args[0] {
6101                    VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6102                    _ => Ok(VmValue::None),
6103                }
6104            }
6105
6106            // Phase 32: GPU Tensor Support
6107            #[cfg(feature = "gpu")]
6108            BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6109            #[cfg(not(feature = "gpu"))]
6110            BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6111
6112            #[cfg(feature = "gpu")]
6113            BuiltinId::ToGpu => {
6114                if args.is_empty() {
6115                    return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6116                }
6117                let gt = self.ensure_gpu_tensor(&args[0])?;
6118                Ok(VmValue::GpuTensor(gt))
6119            }
6120            #[cfg(not(feature = "gpu"))]
6121            BuiltinId::ToGpu => Err(runtime_err(
6122                "GPU operations not available. Build with --features gpu",
6123            )),
6124
6125            #[cfg(feature = "gpu")]
6126            BuiltinId::ToCpu => {
6127                if args.is_empty() {
6128                    return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6129                }
6130                match &args[0] {
6131                    VmValue::GpuTensor(gt) => {
6132                        let cpu = gt.to_cpu().map_err(runtime_err)?;
6133                        Ok(VmValue::Tensor(Arc::new(cpu)))
6134                    }
6135                    _ => Err(runtime_err(format!(
6136                        "to_cpu() expects a gpu_tensor, got {}",
6137                        args[0].type_name()
6138                    ))),
6139                }
6140            }
6141            #[cfg(not(feature = "gpu"))]
6142            BuiltinId::ToCpu => Err(runtime_err(
6143                "GPU operations not available. Build with --features gpu",
6144            )),
6145
6146            #[cfg(feature = "gpu")]
6147            BuiltinId::GpuMatmul => {
6148                if args.len() < 2 {
6149                    return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6150                }
6151                let a = self.ensure_gpu_tensor(&args[0])?;
6152                let b = self.ensure_gpu_tensor(&args[1])?;
6153                let ops = self.get_gpu_ops()?;
6154                let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6155                Ok(VmValue::GpuTensor(Arc::new(result)))
6156            }
6157            #[cfg(not(feature = "gpu"))]
6158            BuiltinId::GpuMatmul => Err(runtime_err(
6159                "GPU operations not available. Build with --features gpu",
6160            )),
6161
6162            #[cfg(feature = "gpu")]
6163            BuiltinId::GpuBatchPredict => {
6164                if args.len() < 2 {
6165                    return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6166                }
6167                match (&args[0], &args[1]) {
6168                    (VmValue::Model(model), VmValue::Tensor(input)) => {
6169                        let batch_size = args.get(2).and_then(|v| match v {
6170                            VmValue::Int(n) => Some(*n as usize),
6171                            _ => None,
6172                        });
6173                        let result =
6174                            tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6175                                .map_err(runtime_err)?;
6176                        Ok(VmValue::Tensor(Arc::new(result)))
6177                    }
6178                    _ => Err(runtime_err(
6179                        "gpu_batch_predict() expects (model, tensor, [batch_size])",
6180                    )),
6181                }
6182            }
6183            #[cfg(not(feature = "gpu"))]
6184            BuiltinId::GpuBatchPredict => Err(runtime_err(
6185                "GPU operations not available. Build with --features gpu",
6186            )),
6187            // Phase 34: AI Agent Framework
6188            #[cfg(feature = "native")]
6189            BuiltinId::Embed => {
6190                if args.is_empty() {
6191                    return Err(runtime_err("embed() requires a text argument"));
6192                }
6193                let text = match &args[0] {
6194                    VmValue::String(s) => s.to_string(),
6195                    _ => return Err(runtime_err("embed() expects a string")),
6196                };
6197                let model = args
6198                    .get(1)
6199                    .and_then(|v| match v {
6200                        VmValue::String(s) => Some(s.to_string()),
6201                        _ => None,
6202                    })
6203                    .unwrap_or_else(|| "text-embedding-3-small".to_string());
6204                let api_key = args
6205                    .get(2)
6206                    .and_then(|v| match v {
6207                        VmValue::String(s) => Some(s.to_string()),
6208                        _ => None,
6209                    })
6210                    .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6211                    .ok_or_else(|| {
6212                        runtime_err(
6213                            "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6214                        )
6215                    })?;
6216                let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6217                    .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6218                Ok(VmValue::Tensor(Arc::new(tensor)))
6219            }
6220            #[cfg(not(feature = "native"))]
6221            BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6222            #[cfg(feature = "native")]
6223            BuiltinId::HttpRequest => {
6224                self.check_permission("network")?;
6225                if args.len() < 2 {
6226                    return Err(runtime_err(
6227                        "http_request(method, url, headers?, body?) expects at least 2 args",
6228                    ));
6229                }
6230                let method = match &args[0] {
6231                    VmValue::String(s) => s.to_string(),
6232                    _ => return Err(runtime_err("http_request() method must be a string")),
6233                };
6234                let url = match &args[1] {
6235                    VmValue::String(s) => s.to_string(),
6236                    _ => return Err(runtime_err("http_request() url must be a string")),
6237                };
6238                let client = reqwest::blocking::Client::new();
6239                let mut builder = match method.to_uppercase().as_str() {
6240                    "GET" => client.get(&url),
6241                    "POST" => client.post(&url),
6242                    "PUT" => client.put(&url),
6243                    "DELETE" => client.delete(&url),
6244                    "PATCH" => client.patch(&url),
6245                    "HEAD" => client.head(&url),
6246                    _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6247                };
6248                // Set headers if provided
6249                if let Some(VmValue::Map(headers)) = args.get(2) {
6250                    for (key, val) in headers.iter() {
6251                        if let VmValue::String(v) = val {
6252                            builder = builder.header(key.as_ref(), v.as_ref());
6253                        }
6254                    }
6255                }
6256                // Set body if provided
6257                if let Some(VmValue::String(body)) = args.get(3) {
6258                    builder = builder.body(body.as_ref().to_string());
6259                }
6260                let resp = builder
6261                    .send()
6262                    .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6263                let status = resp.status().as_u16() as i64;
6264                let body = resp
6265                    .text()
6266                    .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6267                Ok(VmValue::Map(Box::new(vec![
6268                    (Arc::from("status"), VmValue::Int(status)),
6269                    (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6270                ])))
6271            }
6272            #[cfg(not(feature = "native"))]
6273            BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6274            #[cfg(feature = "native")]
6275            BuiltinId::RunAgent => {
6276                self.check_permission("network")?;
6277                if args.len() < 2 {
6278                    return Err(runtime_err(
6279                        "run_agent(agent, message, [history]) expects at least 2 arguments",
6280                    ));
6281                }
6282                let agent_def = match &args[0] {
6283                    VmValue::AgentDef(def) => def.clone(),
6284                    _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6285                };
6286                let message = match &args[1] {
6287                    VmValue::String(s) => s.to_string(),
6288                    _ => return Err(runtime_err("run_agent() second arg must be a string")),
6289                };
6290                // Optional 3rd arg: conversation history as list of [role, content] pairs
6291                let history = if args.len() >= 3 {
6292                    match &args[2] {
6293                        VmValue::List(items) => {
6294                            let mut hist = Vec::new();
6295                            for item in items.iter() {
6296                                if let VmValue::List(pair) = item
6297                                    && pair.len() >= 2
6298                                {
6299                                    let role = match &pair[0] {
6300                                        VmValue::String(s) => s.to_string(),
6301                                        _ => continue,
6302                                    };
6303                                    let content = match &pair[1] {
6304                                        VmValue::String(s) => s.to_string(),
6305                                        _ => continue,
6306                                    };
6307                                    hist.push((role, content));
6308                                }
6309                            }
6310                            Some(hist)
6311                        }
6312                        _ => None,
6313                    }
6314                } else {
6315                    None
6316                };
6317                self.exec_agent_loop(&agent_def, &message, history.as_deref())
6318            }
6319            #[cfg(not(feature = "native"))]
6320            BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6321
6322            // Phase G4: Streaming agent responses
6323            #[cfg(feature = "native")]
6324            BuiltinId::StreamAgent => {
6325                self.check_permission("network")?;
6326                if args.len() < 3 {
6327                    return Err(runtime_err(
6328                        "stream_agent(agent, message, callback) expects 3 arguments",
6329                    ));
6330                }
6331                let agent_def = match &args[0] {
6332                    VmValue::AgentDef(def) => def.clone(),
6333                    _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6334                };
6335                let message = match &args[1] {
6336                    VmValue::String(s) => s.to_string(),
6337                    _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6338                };
6339                let callback = args[2].clone();
6340
6341                let model = &agent_def.model;
6342                let system = agent_def.system_prompt.as_deref();
6343                let base_url = agent_def.base_url.as_deref();
6344                let api_key = agent_def.api_key.as_deref();
6345
6346                let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6347                let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6348                    .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6349
6350                let mut full_text = String::new();
6351                loop {
6352                    match reader.next_chunk() {
6353                        Ok(Some(chunk)) => {
6354                            full_text.push_str(&chunk);
6355                            let chunk_val = VmValue::String(Arc::from(&*chunk));
6356                            let _ = self.call_value(callback.clone(), &[chunk_val]);
6357                        }
6358                        Ok(None) => break,
6359                        Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6360                    }
6361                }
6362
6363                Ok(VmValue::String(Arc::from(&*full_text)))
6364            }
6365            #[cfg(not(feature = "native"))]
6366            BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6367
6368            // Phase E5: Random & Sampling
6369            #[cfg(feature = "native")]
6370            BuiltinId::Random => {
6371                let mut rng = rand::thread_rng();
6372                let val: f64 = rand::Rng::r#gen(&mut rng);
6373                Ok(VmValue::Float(val))
6374            }
6375            #[cfg(not(feature = "native"))]
6376            BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6377            #[cfg(feature = "native")]
6378            BuiltinId::RandomInt => {
6379                if args.len() < 2 {
6380                    return Err(runtime_err("random_int() expects min and max"));
6381                }
6382                let a = match &args[0] {
6383                    VmValue::Int(n) => *n,
6384                    _ => return Err(runtime_err("random_int() expects integers")),
6385                };
6386                let b = match &args[1] {
6387                    VmValue::Int(n) => *n,
6388                    _ => return Err(runtime_err("random_int() expects integers")),
6389                };
6390                if a >= b {
6391                    return Err(runtime_err("random_int() requires min < max"));
6392                }
6393                let mut rng = rand::thread_rng();
6394                let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6395                Ok(VmValue::Int(val))
6396            }
6397            #[cfg(not(feature = "native"))]
6398            BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6399            #[cfg(feature = "native")]
6400            BuiltinId::Sample => {
6401                use rand::seq::SliceRandom;
6402                if args.is_empty() {
6403                    return Err(runtime_err("sample() expects a list and count"));
6404                }
6405                let items = match &args[0] {
6406                    VmValue::List(items) => items,
6407                    _ => return Err(runtime_err("sample() expects a list")),
6408                };
6409                let k = match args.get(1) {
6410                    Some(VmValue::Int(n)) => *n as usize,
6411                    _ => 1,
6412                };
6413                if k > items.len() {
6414                    return Err(runtime_err("sample() count exceeds list length"));
6415                }
6416                let mut rng = rand::thread_rng();
6417                let mut indices: Vec<usize> = (0..items.len()).collect();
6418                indices.partial_shuffle(&mut rng, k);
6419                let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6420                if k == 1 && args.get(1).is_none() {
6421                    Ok(result.into_iter().next().unwrap_or(VmValue::None))
6422                } else {
6423                    Ok(VmValue::List(Box::new(result)))
6424                }
6425            }
6426            #[cfg(not(feature = "native"))]
6427            BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6428
6429            // Phase E6: Math builtins
6430            BuiltinId::Exp => {
6431                let x = match args.first() {
6432                    Some(VmValue::Float(f)) => *f,
6433                    Some(VmValue::Int(n)) => *n as f64,
6434                    _ => return Err(runtime_err("exp() expects a number")),
6435                };
6436                Ok(VmValue::Float(x.exp()))
6437            }
6438            BuiltinId::IsNan => {
6439                let result = match args.first() {
6440                    Some(VmValue::Float(f)) => f.is_nan(),
6441                    _ => false,
6442                };
6443                Ok(VmValue::Bool(result))
6444            }
6445            BuiltinId::IsInfinite => {
6446                let result = match args.first() {
6447                    Some(VmValue::Float(f)) => f.is_infinite(),
6448                    _ => false,
6449                };
6450                Ok(VmValue::Bool(result))
6451            }
6452            BuiltinId::Sign => match args.first() {
6453                Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6454                    1
6455                } else if *n < 0 {
6456                    -1
6457                } else {
6458                    0
6459                })),
6460                Some(VmValue::Float(f)) => {
6461                    if f.is_nan() {
6462                        Ok(VmValue::Float(f64::NAN))
6463                    } else if *f > 0.0 {
6464                        Ok(VmValue::Int(1))
6465                    } else if *f < 0.0 {
6466                        Ok(VmValue::Int(-1))
6467                    } else {
6468                        Ok(VmValue::Int(0))
6469                    }
6470                }
6471                _ => Err(runtime_err("sign() expects a number")),
6472            },
6473            // Phase E8: Table assertion
6474            #[cfg(feature = "native")]
6475            BuiltinId::AssertTableEq => {
6476                if args.len() < 2 {
6477                    return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6478                }
6479                let t1 = match &args[0] {
6480                    VmValue::Table(t) => t,
6481                    _ => {
6482                        return Err(runtime_err(
6483                            "assert_table_eq() first argument must be a table",
6484                        ));
6485                    }
6486                };
6487                let t2 = match &args[1] {
6488                    VmValue::Table(t) => t,
6489                    _ => {
6490                        return Err(runtime_err(
6491                            "assert_table_eq() second argument must be a table",
6492                        ));
6493                    }
6494                };
6495                // Compare schemas
6496                if t1.df.schema() != t2.df.schema() {
6497                    return Err(runtime_err(format!(
6498                        "assert_table_eq: schemas differ\n  left:  {:?}\n  right: {:?}",
6499                        t1.df.schema(),
6500                        t2.df.schema()
6501                    )));
6502                }
6503                // Collect both DataFrames
6504                let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6505                let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6506                // Flatten into rows and compare
6507                let rows1: Vec<String> = batches1
6508                    .iter()
6509                    .flat_map(|b| {
6510                        (0..b.num_rows()).map(move |r| {
6511                            (0..b.num_columns())
6512                                .map(|c| {
6513                                    let col = b.column(c);
6514                                    format!("{:?}", col.slice(r, 1))
6515                                })
6516                                .collect::<Vec<_>>()
6517                                .join(",")
6518                        })
6519                    })
6520                    .collect();
6521                let rows2: Vec<String> = batches2
6522                    .iter()
6523                    .flat_map(|b| {
6524                        (0..b.num_rows()).map(move |r| {
6525                            (0..b.num_columns())
6526                                .map(|c| {
6527                                    let col = b.column(c);
6528                                    format!("{:?}", col.slice(r, 1))
6529                                })
6530                                .collect::<Vec<_>>()
6531                                .join(",")
6532                        })
6533                    })
6534                    .collect();
6535                if rows1.len() != rows2.len() {
6536                    return Err(runtime_err(format!(
6537                        "assert_table_eq: row count differs ({} vs {})",
6538                        rows1.len(),
6539                        rows2.len()
6540                    )));
6541                }
6542                for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
6543                    if r1 != r2 {
6544                        return Err(runtime_err(format!(
6545                            "assert_table_eq: row {} differs\n  left:  {}\n  right: {}",
6546                            i, r1, r2
6547                        )));
6548                    }
6549                }
6550                Ok(VmValue::None)
6551            }
6552            #[cfg(not(feature = "native"))]
6553            BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
6554
6555            // Phase F1: Date/Time builtins
6556            BuiltinId::Today => {
6557                use chrono::{Datelike, TimeZone};
6558                let now = chrono::Utc::now();
6559                let midnight = chrono::Utc
6560                    .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
6561                    .single()
6562                    .ok_or_else(|| runtime_err("Failed to compute today"))?;
6563                Ok(VmValue::DateTime(midnight.timestamp_millis()))
6564            }
6565            BuiltinId::DateAdd => {
6566                if args.len() < 3 {
6567                    return Err(runtime_err("date_add() expects datetime, amount, unit"));
6568                }
6569                let ms = match &args[0] {
6570                    VmValue::DateTime(ms) => *ms,
6571                    VmValue::Int(ms) => *ms,
6572                    _ => return Err(runtime_err("date_add() first arg must be datetime")),
6573                };
6574                let amount = match &args[1] {
6575                    VmValue::Int(n) => *n,
6576                    _ => return Err(runtime_err("date_add() amount must be an integer")),
6577                };
6578                let unit = match &args[2] {
6579                    VmValue::String(s) => s.as_ref(),
6580                    _ => return Err(runtime_err("date_add() unit must be a string")),
6581                };
6582                let offset_ms = match unit {
6583                    "second" | "seconds" => amount * 1000,
6584                    "minute" | "minutes" => amount * 60 * 1000,
6585                    "hour" | "hours" => amount * 3600 * 1000,
6586                    "day" | "days" => amount * 86400 * 1000,
6587                    "week" | "weeks" => amount * 7 * 86400 * 1000,
6588                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6589                };
6590                Ok(VmValue::DateTime(ms + offset_ms))
6591            }
6592            BuiltinId::DateDiff => {
6593                if args.len() < 3 {
6594                    return Err(runtime_err(
6595                        "date_diff() expects datetime1, datetime2, unit",
6596                    ));
6597                }
6598                let ms1 = match &args[0] {
6599                    VmValue::DateTime(ms) => *ms,
6600                    VmValue::Int(ms) => *ms,
6601                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
6602                };
6603                let ms2 = match &args[1] {
6604                    VmValue::DateTime(ms) => *ms,
6605                    VmValue::Int(ms) => *ms,
6606                    _ => return Err(runtime_err("date_diff() args must be datetimes")),
6607                };
6608                let unit = match &args[2] {
6609                    VmValue::String(s) => s.as_ref(),
6610                    _ => return Err(runtime_err("date_diff() unit must be a string")),
6611                };
6612                let diff_ms = ms1 - ms2;
6613                let result = match unit {
6614                    "second" | "seconds" => diff_ms / 1000,
6615                    "minute" | "minutes" => diff_ms / (60 * 1000),
6616                    "hour" | "hours" => diff_ms / (3600 * 1000),
6617                    "day" | "days" => diff_ms / (86400 * 1000),
6618                    "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
6619                    _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
6620                };
6621                Ok(VmValue::Int(result))
6622            }
6623            BuiltinId::DateTrunc => {
6624                if args.len() < 2 {
6625                    return Err(runtime_err("date_trunc() expects datetime and unit"));
6626                }
6627                let ms = match &args[0] {
6628                    VmValue::DateTime(ms) => *ms,
6629                    VmValue::Int(ms) => *ms,
6630                    _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
6631                };
6632                let unit = match &args[1] {
6633                    VmValue::String(s) => s.as_ref(),
6634                    _ => return Err(runtime_err("date_trunc() unit must be a string")),
6635                };
6636                use chrono::{Datelike, TimeZone, Timelike};
6637                let secs = ms / 1000;
6638                let dt = chrono::Utc
6639                    .timestamp_opt(secs, 0)
6640                    .single()
6641                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6642                let truncated = match unit {
6643                    "second" => chrono::Utc
6644                        .with_ymd_and_hms(
6645                            dt.year(),
6646                            dt.month(),
6647                            dt.day(),
6648                            dt.hour(),
6649                            dt.minute(),
6650                            dt.second(),
6651                        )
6652                        .single(),
6653                    "minute" => chrono::Utc
6654                        .with_ymd_and_hms(
6655                            dt.year(),
6656                            dt.month(),
6657                            dt.day(),
6658                            dt.hour(),
6659                            dt.minute(),
6660                            0,
6661                        )
6662                        .single(),
6663                    "hour" => chrono::Utc
6664                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
6665                        .single(),
6666                    "day" => chrono::Utc
6667                        .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
6668                        .single(),
6669                    "month" => chrono::Utc
6670                        .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
6671                        .single(),
6672                    "year" => chrono::Utc
6673                        .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
6674                        .single(),
6675                    _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
6676                };
6677                Ok(VmValue::DateTime(
6678                    truncated
6679                        .ok_or_else(|| runtime_err("Invalid truncation"))?
6680                        .timestamp_millis(),
6681                ))
6682            }
6683            BuiltinId::DateExtract => {
6684                if args.len() < 2 {
6685                    return Err(runtime_err("extract() expects datetime and part"));
6686                }
6687                let ms = match &args[0] {
6688                    VmValue::DateTime(ms) => *ms,
6689                    VmValue::Int(ms) => *ms,
6690                    _ => return Err(runtime_err("extract() first arg must be datetime")),
6691                };
6692                let part = match &args[1] {
6693                    VmValue::String(s) => s.as_ref(),
6694                    _ => return Err(runtime_err("extract() part must be a string")),
6695                };
6696                use chrono::{Datelike, TimeZone, Timelike};
6697                let secs = ms / 1000;
6698                let dt = chrono::Utc
6699                    .timestamp_opt(secs, 0)
6700                    .single()
6701                    .ok_or_else(|| runtime_err("Invalid timestamp"))?;
6702                let val = match part {
6703                    "year" => dt.year() as i64,
6704                    "month" => dt.month() as i64,
6705                    "day" => dt.day() as i64,
6706                    "hour" => dt.hour() as i64,
6707                    "minute" => dt.minute() as i64,
6708                    "second" => dt.second() as i64,
6709                    "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
6710                    "day_of_year" | "doy" => dt.ordinal() as i64,
6711                    _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
6712                };
6713                Ok(VmValue::Int(val))
6714            }
6715
6716            // ── MCP builtins ──
6717            #[cfg(feature = "mcp")]
6718            BuiltinId::McpConnect => {
6719                if args.is_empty() {
6720                    return Err(runtime_err(
6721                        "mcp_connect expects at least 1 argument: command or URL",
6722                    ));
6723                }
6724                let command = match &args[0] {
6725                    VmValue::String(s) => s.to_string(),
6726                    _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
6727                };
6728
6729                // Build sampling callback when tl-ai is available (native feature)
6730                #[cfg(feature = "native")]
6731                let sampling_cb: Option<tl_mcp::SamplingCallback> =
6732                    Some(Arc::new(|req: tl_mcp::SamplingRequest| {
6733                        let model = req
6734                            .model_hint
6735                            .as_deref()
6736                            .unwrap_or("claude-sonnet-4-20250514");
6737                        let messages: Vec<serde_json::Value> = req
6738                            .messages
6739                            .iter()
6740                            .map(|(role, content)| {
6741                                serde_json::json!({"role": role, "content": content})
6742                            })
6743                            .collect();
6744                        let response = tl_ai::chat_with_tools(
6745                            model,
6746                            req.system_prompt.as_deref(),
6747                            &messages,
6748                            &[],  // no tools for sampling
6749                            None, // base_url
6750                            None, // api_key
6751                            None, // output_format
6752                        )
6753                        .map_err(|e| format!("Sampling LLM error: {e}"))?;
6754                        match response {
6755                            tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
6756                                model: model.to_string(),
6757                                content: text,
6758                                stop_reason: Some("endTurn".to_string()),
6759                            }),
6760                            tl_ai::LlmResponse::ToolUse(_) => {
6761                                Err("Sampling does not support tool use".to_string())
6762                            }
6763                        }
6764                    }));
6765
6766                #[cfg(not(feature = "native"))]
6767                let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
6768
6769                // Auto-detect HTTP URL vs subprocess command
6770                let client = if command.starts_with("http://") || command.starts_with("https://") {
6771                    tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
6772                        .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
6773                } else {
6774                    let cmd_args: Vec<String> = args[1..]
6775                        .iter()
6776                        .map(|a| match a {
6777                            VmValue::String(s) => s.to_string(),
6778                            other => format!("{}", other),
6779                        })
6780                        .collect();
6781                    tl_mcp::McpClient::connect_with_sampling(
6782                        &command,
6783                        &cmd_args,
6784                        self.security_policy.as_ref(),
6785                        sampling_cb,
6786                    )
6787                    .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
6788                };
6789                Ok(VmValue::McpClient(Arc::new(client)))
6790            }
6791            #[cfg(not(feature = "mcp"))]
6792            BuiltinId::McpConnect => {
6793                Err(runtime_err("MCP not available. Build with --features mcp"))
6794            }
6795
6796            #[cfg(feature = "mcp")]
6797            BuiltinId::McpListTools => {
6798                if args.is_empty() {
6799                    return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
6800                }
6801                match &args[0] {
6802                    VmValue::McpClient(client) => {
6803                        let tools = client
6804                            .list_tools()
6805                            .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
6806                        let tool_values: Vec<VmValue> = tools
6807                            .iter()
6808                            .map(|tool| {
6809                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6810                                pairs.push((
6811                                    Arc::from("name"),
6812                                    VmValue::String(Arc::from(tool.name.as_ref())),
6813                                ));
6814                                if let Some(desc) = &tool.description {
6815                                    pairs.push((
6816                                        Arc::from("description"),
6817                                        VmValue::String(Arc::from(desc.as_ref())),
6818                                    ));
6819                                }
6820                                let schema_json = serde_json::to_string(tool.input_schema.as_ref())
6821                                    .unwrap_or_default();
6822                                if !schema_json.is_empty() && schema_json != "{}" {
6823                                    pairs.push((
6824                                        Arc::from("input_schema"),
6825                                        VmValue::String(Arc::from(schema_json.as_str())),
6826                                    ));
6827                                }
6828                                VmValue::Map(Box::new(pairs))
6829                            })
6830                            .collect();
6831                        Ok(VmValue::List(Box::new(tool_values)))
6832                    }
6833                    _ => Err(runtime_err(
6834                        "mcp_list_tools: argument must be an mcp_client",
6835                    )),
6836                }
6837            }
6838            #[cfg(not(feature = "mcp"))]
6839            BuiltinId::McpListTools => {
6840                Err(runtime_err("MCP not available. Build with --features mcp"))
6841            }
6842
6843            #[cfg(feature = "mcp")]
6844            BuiltinId::McpCallTool => {
6845                if args.len() < 2 {
6846                    return Err(runtime_err(
6847                        "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
6848                    ));
6849                }
6850                let client = match &args[0] {
6851                    VmValue::McpClient(c) => c.clone(),
6852                    _ => {
6853                        return Err(runtime_err(
6854                            "mcp_call_tool: first argument must be an mcp_client",
6855                        ));
6856                    }
6857                };
6858                let tool_name = match &args[1] {
6859                    VmValue::String(s) => s.to_string(),
6860                    _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
6861                };
6862                let arguments = if args.len() > 2 {
6863                    vm_value_to_json(&args[2])
6864                } else {
6865                    serde_json::Value::Object(serde_json::Map::new())
6866                };
6867                let result = client
6868                    .call_tool(&tool_name, arguments)
6869                    .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
6870                let mut content_parts: Vec<VmValue> = Vec::new();
6871                for content in &result.content {
6872                    if let Some(text) = content.as_text() {
6873                        content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
6874                    }
6875                }
6876                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
6877                if content_parts.len() == 1 {
6878                    pairs.push((
6879                        Arc::from("content"),
6880                        content_parts.into_iter().next().unwrap(),
6881                    ));
6882                } else {
6883                    pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
6884                }
6885                pairs.push((
6886                    Arc::from("is_error"),
6887                    VmValue::Bool(result.is_error.unwrap_or(false)),
6888                ));
6889                Ok(VmValue::Map(Box::new(pairs)))
6890            }
6891            #[cfg(not(feature = "mcp"))]
6892            BuiltinId::McpCallTool => {
6893                Err(runtime_err("MCP not available. Build with --features mcp"))
6894            }
6895
6896            #[cfg(feature = "mcp")]
6897            BuiltinId::McpDisconnect => {
6898                if args.is_empty() {
6899                    return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
6900                }
6901                match &args[0] {
6902                    VmValue::McpClient(_) => Ok(VmValue::None),
6903                    _ => Err(runtime_err(
6904                        "mcp_disconnect: argument must be an mcp_client",
6905                    )),
6906                }
6907            }
6908            #[cfg(not(feature = "mcp"))]
6909            BuiltinId::McpDisconnect => {
6910                Err(runtime_err("MCP not available. Build with --features mcp"))
6911            }
6912
6913            #[cfg(feature = "mcp")]
6914            BuiltinId::McpPing => {
6915                if args.is_empty() {
6916                    return Err(runtime_err("mcp_ping expects 1 argument: client"));
6917                }
6918                match &args[0] {
6919                    VmValue::McpClient(client) => {
6920                        client
6921                            .ping()
6922                            .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
6923                        Ok(VmValue::Bool(true))
6924                    }
6925                    _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
6926                }
6927            }
6928            #[cfg(not(feature = "mcp"))]
6929            BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
6930
6931            #[cfg(feature = "mcp")]
6932            BuiltinId::McpServerInfo => {
6933                if args.is_empty() {
6934                    return Err(runtime_err("mcp_server_info expects 1 argument: client"));
6935                }
6936                match &args[0] {
6937                    VmValue::McpClient(client) => match client.server_info() {
6938                        Some(info) => {
6939                            let pairs: Vec<(Arc<str>, VmValue)> = vec![
6940                                (
6941                                    Arc::from("name"),
6942                                    VmValue::String(Arc::from(info.server_info.name.as_str())),
6943                                ),
6944                                (
6945                                    Arc::from("version"),
6946                                    VmValue::String(Arc::from(info.server_info.version.as_str())),
6947                                ),
6948                            ];
6949                            Ok(VmValue::Map(Box::new(pairs)))
6950                        }
6951                        None => Ok(VmValue::None),
6952                    },
6953                    _ => Err(runtime_err(
6954                        "mcp_server_info: argument must be an mcp_client",
6955                    )),
6956                }
6957            }
6958            #[cfg(not(feature = "mcp"))]
6959            BuiltinId::McpServerInfo => {
6960                Err(runtime_err("MCP not available. Build with --features mcp"))
6961            }
6962
6963            #[cfg(feature = "mcp")]
6964            BuiltinId::McpServe => {
6965                self.check_permission("network")?;
6966                if args.is_empty() {
6967                    return Err(runtime_err(
6968                        "mcp_serve expects 1 argument: list of tool definitions",
6969                    ));
6970                }
6971                let tool_list = match &args[0] {
6972                    VmValue::List(items) => items.as_ref().clone(),
6973                    _ => {
6974                        return Err(runtime_err(
6975                            "mcp_serve: argument must be a list of tool maps",
6976                        ));
6977                    }
6978                };
6979
6980                // Extract tool definitions and function values
6981                let mut channel_tools = Vec::new();
6982                let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
6983
6984                for item in &tool_list {
6985                    let pairs = match item {
6986                        VmValue::Map(p) => p.as_ref(),
6987                        _ => {
6988                            return Err(runtime_err(
6989                                "mcp_serve: each tool must be a map with name, description, handler",
6990                            ));
6991                        }
6992                    };
6993                    let mut name = String::new();
6994                    let mut description = String::new();
6995                    let mut handler = None;
6996                    let mut input_schema = serde_json::json!({"type": "object"});
6997
6998                    for (k, v) in pairs {
6999                        match k.as_ref() {
7000                            "name" => {
7001                                if let VmValue::String(s) = v {
7002                                    name = s.to_string();
7003                                }
7004                            }
7005                            "description" => {
7006                                if let VmValue::String(s) = v {
7007                                    description = s.to_string();
7008                                }
7009                            }
7010                            "handler" => {
7011                                handler = Some(v.clone());
7012                            }
7013                            "input_schema" | "parameters" => {
7014                                if let VmValue::String(s) = v
7015                                    && let Ok(parsed) =
7016                                        serde_json::from_str::<serde_json::Value>(s.as_ref())
7017                                {
7018                                    input_schema = parsed;
7019                                }
7020                            }
7021                            _ => {}
7022                        }
7023                    }
7024
7025                    if name.is_empty() {
7026                        return Err(runtime_err("mcp_serve: tool missing 'name'"));
7027                    }
7028                    if let Some(h) = handler {
7029                        tool_handlers.insert(name.clone(), h);
7030                    }
7031
7032                    channel_tools.push(tl_mcp::server::ChannelToolDef {
7033                        name,
7034                        description,
7035                        input_schema,
7036                    });
7037                }
7038
7039                // Build server with channel-based tools
7040                let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7041                    .name("tl-mcp-server")
7042                    .version("1.0.0")
7043                    .channel_tools(channel_tools);
7044                let server_handler = builder.build();
7045
7046                // Start server on background thread
7047                let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7048
7049                // Main dispatch loop: process tool call requests from the MCP server
7050                while let Ok(req) = rx.recv() {
7051                    let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7052                        // Convert JSON args to VmValue args
7053                        let call_args = self.json_to_vm_args(&req.arguments);
7054                        match self.call_value(func.clone(), &call_args) {
7055                            Ok(val) => {
7056                                // Convert VmValue to JSON-friendly string
7057                                Ok(serde_json::json!(format!("{val}")))
7058                            }
7059                            Err(e) => Err(format!("{e}")),
7060                        }
7061                    } else {
7062                        Err(format!("Unknown tool: {}", req.tool_name))
7063                    };
7064                    let _ = req.response_tx.send(result);
7065                }
7066
7067                Ok(VmValue::None)
7068            }
7069            #[cfg(not(feature = "mcp"))]
7070            BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7071
7072            // ── MCP Resources & Prompts ──
7073            #[cfg(feature = "mcp")]
7074            BuiltinId::McpListResources => {
7075                if args.is_empty() {
7076                    return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7077                }
7078                match &args[0] {
7079                    VmValue::McpClient(client) => {
7080                        let resources = client
7081                            .list_resources()
7082                            .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7083                        let vals: Vec<VmValue> = resources
7084                            .iter()
7085                            .map(|r| {
7086                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7087                                pairs.push((
7088                                    Arc::from("uri"),
7089                                    VmValue::String(Arc::from(r.uri.as_str())),
7090                                ));
7091                                pairs.push((
7092                                    Arc::from("name"),
7093                                    VmValue::String(Arc::from(r.name.as_str())),
7094                                ));
7095                                if let Some(desc) = &r.description {
7096                                    pairs.push((
7097                                        Arc::from("description"),
7098                                        VmValue::String(Arc::from(desc.as_str())),
7099                                    ));
7100                                }
7101                                if let Some(mime) = &r.mime_type {
7102                                    pairs.push((
7103                                        Arc::from("mime_type"),
7104                                        VmValue::String(Arc::from(mime.as_str())),
7105                                    ));
7106                                }
7107                                VmValue::Map(Box::new(pairs))
7108                            })
7109                            .collect();
7110                        Ok(VmValue::List(Box::new(vals)))
7111                    }
7112                    _ => Err(runtime_err(
7113                        "mcp_list_resources: argument must be an mcp_client",
7114                    )),
7115                }
7116            }
7117            #[cfg(not(feature = "mcp"))]
7118            BuiltinId::McpListResources => {
7119                Err(runtime_err("MCP not available. Build with --features mcp"))
7120            }
7121
7122            #[cfg(feature = "mcp")]
7123            BuiltinId::McpReadResource => {
7124                if args.len() < 2 {
7125                    return Err(runtime_err(
7126                        "mcp_read_resource expects 2 arguments: client, uri",
7127                    ));
7128                }
7129                let client = match &args[0] {
7130                    VmValue::McpClient(c) => c.clone(),
7131                    _ => {
7132                        return Err(runtime_err(
7133                            "mcp_read_resource: first argument must be an mcp_client",
7134                        ));
7135                    }
7136                };
7137                let uri = match &args[1] {
7138                    VmValue::String(s) => s.to_string(),
7139                    _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7140                };
7141                let result = client
7142                    .read_resource(&uri)
7143                    .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7144                // Serialize ResourceContents via JSON to avoid direct rmcp type dependency
7145                let contents: Vec<VmValue> = result
7146                    .contents
7147                    .iter()
7148                    .map(|content| {
7149                        let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7150                        let json = serde_json::to_value(content).unwrap_or_default();
7151                        if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7152                            pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7153                        }
7154                        if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7155                            pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7156                        }
7157                        if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7158                            pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7159                        }
7160                        if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7161                            pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7162                        }
7163                        VmValue::Map(Box::new(pairs))
7164                    })
7165                    .collect();
7166                if contents.len() == 1 {
7167                    Ok(contents.into_iter().next().unwrap())
7168                } else {
7169                    Ok(VmValue::List(Box::new(contents)))
7170                }
7171            }
7172            #[cfg(not(feature = "mcp"))]
7173            BuiltinId::McpReadResource => {
7174                Err(runtime_err("MCP not available. Build with --features mcp"))
7175            }
7176
7177            #[cfg(feature = "mcp")]
7178            BuiltinId::McpListPrompts => {
7179                if args.is_empty() {
7180                    return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7181                }
7182                match &args[0] {
7183                    VmValue::McpClient(client) => {
7184                        let prompts = client
7185                            .list_prompts()
7186                            .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7187                        let vals: Vec<VmValue> = prompts
7188                            .iter()
7189                            .map(|p| {
7190                                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7191                                pairs.push((
7192                                    Arc::from("name"),
7193                                    VmValue::String(Arc::from(p.name.as_str())),
7194                                ));
7195                                if let Some(desc) = &p.description {
7196                                    pairs.push((
7197                                        Arc::from("description"),
7198                                        VmValue::String(Arc::from(desc.as_str())),
7199                                    ));
7200                                }
7201                                if let Some(prompt_args) = &p.arguments {
7202                                    let arg_vals: Vec<VmValue> = prompt_args
7203                                        .iter()
7204                                        .map(|a| {
7205                                            let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7206                                                Vec::new();
7207                                            arg_pairs.push((
7208                                                Arc::from("name"),
7209                                                VmValue::String(Arc::from(a.name.as_str())),
7210                                            ));
7211                                            if let Some(desc) = &a.description {
7212                                                arg_pairs.push((
7213                                                    Arc::from("description"),
7214                                                    VmValue::String(Arc::from(desc.as_str())),
7215                                                ));
7216                                            }
7217                                            arg_pairs.push((
7218                                                Arc::from("required"),
7219                                                VmValue::Bool(a.required.unwrap_or(false)),
7220                                            ));
7221                                            VmValue::Map(Box::new(arg_pairs))
7222                                        })
7223                                        .collect();
7224                                    pairs.push((
7225                                        Arc::from("arguments"),
7226                                        VmValue::List(Box::new(arg_vals)),
7227                                    ));
7228                                }
7229                                VmValue::Map(Box::new(pairs))
7230                            })
7231                            .collect();
7232                        Ok(VmValue::List(Box::new(vals)))
7233                    }
7234                    _ => Err(runtime_err(
7235                        "mcp_list_prompts: argument must be an mcp_client",
7236                    )),
7237                }
7238            }
7239            #[cfg(not(feature = "mcp"))]
7240            BuiltinId::McpListPrompts => {
7241                Err(runtime_err("MCP not available. Build with --features mcp"))
7242            }
7243
7244            #[cfg(feature = "mcp")]
7245            BuiltinId::McpGetPrompt => {
7246                if args.len() < 2 {
7247                    return Err(runtime_err(
7248                        "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7249                    ));
7250                }
7251                let client = match &args[0] {
7252                    VmValue::McpClient(c) => c.clone(),
7253                    _ => {
7254                        return Err(runtime_err(
7255                            "mcp_get_prompt: first argument must be an mcp_client",
7256                        ));
7257                    }
7258                };
7259                let name = match &args[1] {
7260                    VmValue::String(s) => s.to_string(),
7261                    _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7262                };
7263                let prompt_args = if args.len() > 2 {
7264                    let json = vm_value_to_json(&args[2]);
7265                    json.as_object().cloned()
7266                } else {
7267                    None
7268                };
7269                let result = client
7270                    .get_prompt(&name, prompt_args)
7271                    .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7272                let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7273                if let Some(desc) = &result.description {
7274                    pairs.push((
7275                        Arc::from("description"),
7276                        VmValue::String(Arc::from(desc.as_str())),
7277                    ));
7278                }
7279                // Serialize PromptMessage via JSON to avoid direct rmcp type dependency
7280                let messages: Vec<VmValue> = result
7281                    .messages
7282                    .iter()
7283                    .map(|m| {
7284                        let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7285                        let msg_json = serde_json::to_value(m).unwrap_or_default();
7286                        // role is serialized as "user" or "assistant"
7287                        if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7288                            msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7289                        }
7290                        // content is an object with "type" field; extract text if it's a text message
7291                        if let Some(content) = msg_json.get("content") {
7292                            if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7293                                msg_pairs
7294                                    .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7295                            } else {
7296                                let content_str = content.to_string();
7297                                msg_pairs.push((
7298                                    Arc::from("content"),
7299                                    VmValue::String(Arc::from(content_str.as_str())),
7300                                ));
7301                            }
7302                        }
7303                        VmValue::Map(Box::new(msg_pairs))
7304                    })
7305                    .collect();
7306                pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7307                Ok(VmValue::Map(Box::new(pairs)))
7308            }
7309            #[cfg(not(feature = "mcp"))]
7310            BuiltinId::McpGetPrompt => {
7311                Err(runtime_err("MCP not available. Build with --features mcp"))
7312            }
7313        }
7314    }
7315
7316    // ── AI helpers ──
7317
7318    fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7319        match val {
7320            VmValue::List(items) => items
7321                .iter()
7322                .map(|item| match item {
7323                    VmValue::Int(n) => Ok(*n as f64),
7324                    VmValue::Float(f) => Ok(*f),
7325                    _ => Err(runtime_err("Expected number in list")),
7326                })
7327                .collect(),
7328            VmValue::Int(n) => Ok(vec![*n as f64]),
7329            VmValue::Float(f) => Ok(vec![*f]),
7330            _ => Err(runtime_err("Expected a list of numbers")),
7331        }
7332    }
7333
7334    fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7335        match val {
7336            VmValue::List(items) => items
7337                .iter()
7338                .map(|item| match item {
7339                    VmValue::Int(n) => Ok(*n as usize),
7340                    _ => Err(runtime_err("Expected integer in shape list")),
7341                })
7342                .collect(),
7343            _ => Err(runtime_err("Expected a list of integers for shape")),
7344        }
7345    }
7346
7347    #[cfg(feature = "native")]
7348    fn handle_train(
7349        &mut self,
7350        frame_idx: usize,
7351        algo_const: u8,
7352        config_const: u8,
7353    ) -> Result<VmValue, TlError> {
7354        let frame = &self.frames[frame_idx];
7355        let algorithm = match &frame.prototype.constants[algo_const as usize] {
7356            Constant::String(s) => s.to_string(),
7357            _ => return Err(runtime_err("Expected string constant for algorithm")),
7358        };
7359        let config_args = match &frame.prototype.constants[config_const as usize] {
7360            Constant::AstExprList(args) => args.clone(),
7361            _ => return Err(runtime_err("Expected AST expr list for train config")),
7362        };
7363
7364        // Extract config values
7365        let mut data_val = None;
7366        let mut target_name = None;
7367        let mut feature_names: Vec<String> = Vec::new();
7368
7369        for arg in &config_args {
7370            if let AstExpr::NamedArg { name, value } = arg {
7371                match name.as_str() {
7372                    "data" => {
7373                        data_val = Some(self.eval_ast_to_vm(value)?);
7374                    }
7375                    "target" => {
7376                        if let AstExpr::String(s) = value.as_ref() {
7377                            target_name = Some(s.clone());
7378                        }
7379                    }
7380                    "features" => {
7381                        if let AstExpr::List(items) = value.as_ref() {
7382                            for item in items {
7383                                if let AstExpr::String(s) = item {
7384                                    feature_names.push(s.clone());
7385                                }
7386                            }
7387                        }
7388                    }
7389                    _ => {}
7390                }
7391            }
7392        }
7393
7394        // Build training config from table data
7395        let table = match data_val {
7396            Some(VmValue::Table(t)) => t,
7397            _ => return Err(runtime_err("train: data must be a table")),
7398        };
7399        let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7400
7401        // Collect table to Arrow batches
7402        let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7403        if batches.is_empty() {
7404            return Err(runtime_err("train: empty dataset"));
7405        }
7406
7407        // Determine feature columns if not specified
7408        let batch = &batches[0];
7409        let schema = batch.schema();
7410        if feature_names.is_empty() {
7411            for field in schema.fields() {
7412                if field.name() != &target {
7413                    feature_names.push(field.name().clone());
7414                }
7415            }
7416        }
7417
7418        // Extract feature data and target data as f64 arrays
7419        let n_rows = batch.num_rows();
7420        let n_features = feature_names.len();
7421        let mut features_data = Vec::with_capacity(n_rows * n_features);
7422        let mut target_data = Vec::with_capacity(n_rows);
7423
7424        for col_name in &feature_names {
7425            let col_idx = schema
7426                .index_of(col_name)
7427                .map_err(|_| runtime_err(format!("Column not found: {col_name}")))?;
7428            let col_arr = batch.column(col_idx);
7429            Self::extract_f64_column(col_arr, &mut features_data)?;
7430        }
7431
7432        // Extract target column
7433        let target_idx = schema
7434            .index_of(&target)
7435            .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7436        let target_arr = batch.column(target_idx);
7437        Self::extract_f64_column(target_arr, &mut target_data)?;
7438
7439        // Reshape features: [col1_row1, col1_row2, ..., col2_row1, ...] → row-major
7440        let mut row_major = Vec::with_capacity(n_rows * n_features);
7441        for row in 0..n_rows {
7442            for col in 0..n_features {
7443                row_major.push(features_data[col * n_rows + row]);
7444            }
7445        }
7446
7447        let features_tensor = tl_ai::TlTensor::from_vec(row_major, &[n_rows, n_features])
7448            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7449        let target_tensor = tl_ai::TlTensor::from_vec(target_data, &[n_rows])
7450            .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7451
7452        let config = tl_ai::TrainConfig {
7453            features: features_tensor,
7454            target: target_tensor,
7455            feature_names: feature_names.clone(),
7456            target_name: target.clone(),
7457            model_name: algorithm.clone(),
7458            split_ratio: 0.8,
7459            hyperparams: std::collections::HashMap::new(),
7460        };
7461
7462        let model = tl_ai::train(&algorithm, &config)
7463            .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7464
7465        Ok(VmValue::Model(Arc::new(model)))
7466    }
7467
7468    #[cfg(feature = "native")]
7469    fn extract_f64_column(
7470        col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7471        out: &mut Vec<f64>,
7472    ) -> Result<(), TlError> {
7473        use tl_data::datafusion::arrow::array::{
7474            Array, Float32Array, Float64Array, Int32Array, Int64Array,
7475        };
7476        let len = col.len();
7477        if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
7478            for i in 0..len {
7479                out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
7480            }
7481        } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
7482            for i in 0..len {
7483                out.push(if arr.is_null(i) {
7484                    0.0
7485                } else {
7486                    arr.value(i) as f64
7487                });
7488            }
7489        } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
7490            for i in 0..len {
7491                out.push(if arr.is_null(i) {
7492                    0.0
7493                } else {
7494                    arr.value(i) as f64
7495                });
7496            }
7497        } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
7498            for i in 0..len {
7499                out.push(if arr.is_null(i) {
7500                    0.0
7501                } else {
7502                    arr.value(i) as f64
7503                });
7504            }
7505        } else {
7506            return Err(runtime_err(
7507                "Column must be numeric (int32, int64, float32, float64)",
7508            ));
7509        }
7510        Ok(())
7511    }
7512
7513    #[cfg(feature = "native")]
7514    fn handle_pipeline_exec(
7515        &mut self,
7516        frame_idx: usize,
7517        name_const: u8,
7518        config_const: u8,
7519    ) -> Result<VmValue, TlError> {
7520        let frame = &self.frames[frame_idx];
7521        let name = match &frame.prototype.constants[name_const as usize] {
7522            Constant::String(s) => s.to_string(),
7523            _ => return Err(runtime_err("Expected string constant for pipeline name")),
7524        };
7525
7526        let mut schedule = None;
7527        let mut timeout_ms = None;
7528        let mut retries = 0u32;
7529
7530        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7531            for arg in args {
7532                if let AstExpr::NamedArg { name: key, value } = arg {
7533                    match key.as_str() {
7534                        "schedule" => {
7535                            if let AstExpr::String(s) = value.as_ref() {
7536                                schedule = Some(s.clone());
7537                            }
7538                        }
7539                        "timeout" => {
7540                            if let AstExpr::String(s) = value.as_ref() {
7541                                timeout_ms = tl_stream::parse_duration(s).ok();
7542                            }
7543                        }
7544                        "retries" => {
7545                            if let AstExpr::Int(n) = value.as_ref() {
7546                                retries = *n as u32;
7547                            }
7548                        }
7549                        _ => {}
7550                    }
7551                }
7552            }
7553        }
7554
7555        let def = tl_stream::PipelineDef {
7556            name,
7557            schedule,
7558            timeout_ms,
7559            retries,
7560        };
7561
7562        self.output
7563            .push(format!("Pipeline '{}': success", def.name));
7564        Ok(VmValue::PipelineDef(Arc::new(def)))
7565    }
7566
7567    #[cfg(feature = "native")]
7568    fn handle_stream_exec(
7569        &mut self,
7570        frame_idx: usize,
7571        config_const: u8,
7572    ) -> Result<VmValue, TlError> {
7573        let frame = &self.frames[frame_idx];
7574        let config_args = match &frame.prototype.constants[config_const as usize] {
7575            Constant::AstExprList(args) => args.clone(),
7576            _ => return Err(runtime_err("Expected AST expr list for stream config")),
7577        };
7578
7579        let mut name = String::new();
7580        let mut window = None;
7581        let mut watermark_ms = None;
7582
7583        for arg in &config_args {
7584            if let AstExpr::NamedArg { name: key, value } = arg {
7585                match key.as_str() {
7586                    "name" => {
7587                        if let AstExpr::String(s) = value.as_ref() {
7588                            name = s.clone();
7589                        }
7590                    }
7591                    "window" => {
7592                        if let AstExpr::String(s) = value.as_ref() {
7593                            window = Self::parse_window_type(s);
7594                        }
7595                    }
7596                    "watermark" => {
7597                        if let AstExpr::String(s) = value.as_ref() {
7598                            watermark_ms = tl_stream::parse_duration(s).ok();
7599                        }
7600                    }
7601                    _ => {}
7602                }
7603            }
7604        }
7605
7606        let def = tl_stream::StreamDef {
7607            name: name.clone(),
7608            window,
7609            watermark_ms,
7610        };
7611
7612        self.output.push(format!("Stream '{}' declared", name));
7613        Ok(VmValue::StreamDef(Arc::new(def)))
7614    }
7615
7616    #[cfg(feature = "native")]
7617    fn handle_agent_exec(
7618        &mut self,
7619        frame_idx: usize,
7620        name_const: u8,
7621        config_const: u8,
7622    ) -> Result<VmValue, TlError> {
7623        let frame = &self.frames[frame_idx];
7624        let name = match &frame.prototype.constants[name_const as usize] {
7625            Constant::String(s) => s.to_string(),
7626            _ => return Err(runtime_err("Expected string constant for agent name")),
7627        };
7628
7629        let mut model = String::new();
7630        let mut system_prompt = None;
7631        let mut max_turns = 10u32;
7632        let mut temperature = None;
7633        let mut max_tokens = None;
7634        let mut base_url = None;
7635        let mut api_key = None;
7636        let mut output_format = None;
7637        let mut tools = Vec::new();
7638        #[cfg(feature = "mcp")]
7639        let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
7640
7641        if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
7642            for arg in args {
7643                if let AstExpr::NamedArg { name: key, value } = arg {
7644                    if let Some(tool_name) = key.strip_prefix("tool:") {
7645                        // Tool definition — extract description and parameters from map expr
7646                        let (desc, params) = Self::extract_tool_from_ast(value);
7647                        tools.push(tl_stream::AgentTool {
7648                            name: tool_name.to_string(),
7649                            description: desc,
7650                            parameters: params,
7651                        });
7652                    } else if key.starts_with("mcp_server:") {
7653                        // MCP server reference — look up variable in globals
7654                        #[cfg(feature = "mcp")]
7655                        if let AstExpr::Ident(var_name) = value.as_ref()
7656                            && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
7657                        {
7658                            mcp_clients.push(client.clone());
7659                        }
7660                    } else {
7661                        match key.as_str() {
7662                            "model" => {
7663                                if let AstExpr::String(s) = value.as_ref() {
7664                                    model = s.clone();
7665                                }
7666                            }
7667                            "system" => {
7668                                if let AstExpr::String(s) = value.as_ref() {
7669                                    system_prompt = Some(s.clone());
7670                                }
7671                            }
7672                            "max_turns" => {
7673                                if let AstExpr::Int(n) = value.as_ref() {
7674                                    max_turns = *n as u32;
7675                                }
7676                            }
7677                            "temperature" => {
7678                                if let AstExpr::Float(f) = value.as_ref() {
7679                                    temperature = Some(*f);
7680                                }
7681                            }
7682                            "max_tokens" => {
7683                                if let AstExpr::Int(n) = value.as_ref() {
7684                                    max_tokens = Some(*n as u32);
7685                                }
7686                            }
7687                            "base_url" => {
7688                                if let AstExpr::String(s) = value.as_ref() {
7689                                    base_url = Some(s.clone());
7690                                }
7691                            }
7692                            "api_key" => {
7693                                if let AstExpr::String(s) = value.as_ref() {
7694                                    api_key = Some(s.clone());
7695                                }
7696                            }
7697                            "output_format" => {
7698                                if let AstExpr::String(s) = value.as_ref() {
7699                                    output_format = Some(s.clone());
7700                                }
7701                            }
7702                            _ => {}
7703                        }
7704                    }
7705                }
7706            }
7707        }
7708
7709        let def = tl_stream::AgentDef {
7710            name: name.clone(),
7711            model,
7712            system_prompt,
7713            tools,
7714            max_turns,
7715            temperature,
7716            max_tokens,
7717            base_url,
7718            api_key,
7719            output_format,
7720        };
7721
7722        // Store MCP clients for this agent
7723        #[cfg(feature = "mcp")]
7724        if !mcp_clients.is_empty() {
7725            self.mcp_agent_clients.insert(name.clone(), mcp_clients);
7726        }
7727
7728        Ok(VmValue::AgentDef(Arc::new(def)))
7729    }
7730
7731    #[cfg(feature = "native")]
7732    fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
7733        let mut desc = String::new();
7734        let mut params = serde_json::Value::Object(serde_json::Map::new());
7735        if let AstExpr::Map(pairs) = expr {
7736            for (key_expr, val_expr) in pairs {
7737                if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
7738                    match key.as_str() {
7739                        "description" => {
7740                            if let AstExpr::String(s) = val_expr {
7741                                desc = s.clone();
7742                            }
7743                        }
7744                        "parameters" => {
7745                            params = Self::ast_to_json(val_expr);
7746                        }
7747                        _ => {}
7748                    }
7749                }
7750            }
7751        }
7752        (desc, params)
7753    }
7754
7755    #[cfg(feature = "native")]
7756    fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
7757        match expr {
7758            AstExpr::String(s) => serde_json::Value::String(s.clone()),
7759            AstExpr::Int(n) => serde_json::json!(*n),
7760            AstExpr::Float(f) => serde_json::json!(*f),
7761            AstExpr::Bool(b) => serde_json::Value::Bool(*b),
7762            AstExpr::None => serde_json::Value::Null,
7763            AstExpr::List(items) => {
7764                serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
7765            }
7766            AstExpr::Map(pairs) => {
7767                let mut map = serde_json::Map::new();
7768                for (k, v) in pairs {
7769                    let key = match k {
7770                        AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
7771                        _ => format!("{k:?}"),
7772                    };
7773                    map.insert(key, Self::ast_to_json(v));
7774                }
7775                serde_json::Value::Object(map)
7776            }
7777            _ => serde_json::Value::Null,
7778        }
7779    }
7780
7781    #[cfg(feature = "native")]
7782    fn exec_agent_loop(
7783        &mut self,
7784        agent_def: &tl_stream::AgentDef,
7785        user_message: &str,
7786        history: Option<&[(String, String)]>,
7787    ) -> Result<VmValue, TlError> {
7788        use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
7789
7790        let model = &agent_def.model;
7791        let system = agent_def.system_prompt.as_deref();
7792        let base_url = agent_def.base_url.as_deref();
7793        let api_key = agent_def.api_key.as_deref();
7794
7795        let provider = if model.starts_with("claude") {
7796            "anthropic"
7797        } else {
7798            "openai"
7799        };
7800
7801        // Build tools JSON in OpenAI format from TL-declared tools
7802        #[allow(unused_mut)]
7803        let mut tools_json: Vec<serde_json::Value> = agent_def
7804            .tools
7805            .iter()
7806            .map(|t| {
7807                serde_json::json!({
7808                    "type": "function",
7809                    "function": {
7810                        "name": t.name,
7811                        "description": t.description,
7812                        "parameters": t.parameters
7813                    }
7814                })
7815            })
7816            .collect();
7817
7818        // Add MCP tools from connected servers
7819        #[cfg(feature = "mcp")]
7820        let mcp_clients = self
7821            .mcp_agent_clients
7822            .get(&agent_def.name)
7823            .cloned()
7824            .unwrap_or_default();
7825        #[cfg(feature = "mcp")]
7826        let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
7827            let mut dispatch = std::collections::HashMap::new();
7828            for (client_idx, client) in mcp_clients.iter().enumerate() {
7829                if let Ok(mcp_tools) = client.list_tools() {
7830                    for tool in mcp_tools {
7831                        let tool_name = tool.name.to_string();
7832                        tools_json.push(serde_json::json!({
7833                            "type": "function",
7834                            "function": {
7835                                "name": &tool_name,
7836                                "description": tool.description.as_deref().unwrap_or(""),
7837                                "parameters": serde_json::Value::Object((*tool.input_schema).clone())
7838                            }
7839                        }));
7840                        dispatch.insert(tool_name, client_idx);
7841                    }
7842                }
7843            }
7844            dispatch
7845        };
7846
7847        // Seed messages with history if provided
7848        let mut messages: Vec<serde_json::Value> = Vec::new();
7849        if let Some(hist) = history {
7850            for (role, content) in hist {
7851                messages.push(serde_json::json!({"role": role, "content": content}));
7852            }
7853        }
7854        // Add the current user message
7855        messages.push(serde_json::json!({
7856            "role": "user",
7857            "content": user_message
7858        }));
7859
7860        for turn in 0..agent_def.max_turns {
7861            let response = chat_with_tools(
7862                model,
7863                system,
7864                &messages,
7865                &tools_json,
7866                base_url,
7867                api_key,
7868                agent_def.output_format.as_deref(),
7869            )
7870            .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
7871
7872            match response {
7873                LlmResponse::Text(text) => {
7874                    // Add assistant response to history
7875                    messages.push(serde_json::json!({"role": "assistant", "content": &text}));
7876
7877                    // Build conversation history as list of [role, content] pairs
7878                    let history_list: Vec<VmValue> = messages
7879                        .iter()
7880                        .filter_map(|m| {
7881                            let role = m["role"].as_str()?;
7882                            let content = m["content"].as_str()?;
7883                            Some(VmValue::List(Box::new(vec![
7884                                VmValue::String(Arc::from(role)),
7885                                VmValue::String(Arc::from(content)),
7886                            ])))
7887                        })
7888                        .collect();
7889
7890                    // Agent completed — return result map with history
7891                    let result = VmValue::Map(Box::new(vec![
7892                        (
7893                            Arc::from("response"),
7894                            VmValue::String(Arc::from(text.as_str())),
7895                        ),
7896                        (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
7897                        (Arc::from("history"), VmValue::List(Box::new(history_list))),
7898                    ]));
7899
7900                    // Call on_complete lifecycle hook if defined
7901                    let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
7902                    if let Some(hook) = self.globals.get(&hook_name).cloned() {
7903                        let _ = self.call_value(hook, std::slice::from_ref(&result));
7904                    }
7905
7906                    return Ok(result);
7907                }
7908                LlmResponse::ToolUse(tool_calls) => {
7909                    // Add assistant message with tool calls for context
7910                    let tc_json: Vec<serde_json::Value> = tool_calls
7911                        .iter()
7912                        .map(|tc| {
7913                            serde_json::json!({
7914                                "id": tc.id,
7915                                "type": "function",
7916                                "function": {
7917                                    "name": tc.name,
7918                                    "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
7919                                }
7920                            })
7921                        })
7922                        .collect();
7923                    messages.push(serde_json::json!({
7924                        "role": "assistant",
7925                        "tool_calls": tc_json
7926                    }));
7927
7928                    // Build declared tool names (TL tools + MCP tools)
7929                    #[allow(unused_mut)]
7930                    let mut declared: Vec<String> =
7931                        agent_def.tools.iter().map(|t| t.name.clone()).collect();
7932                    #[cfg(feature = "mcp")]
7933                    {
7934                        for name in mcp_tool_dispatch.keys() {
7935                            declared.push(name.clone());
7936                        }
7937                    }
7938
7939                    // Execute each tool call
7940                    let mut results: Vec<(String, String)> = Vec::new();
7941                    for tc in &tool_calls {
7942                        if !declared.iter().any(|d| d == &tc.name) {
7943                            results.push((
7944                                tc.name.clone(),
7945                                format!("Error: '{}' not in declared tools", tc.name),
7946                            ));
7947                            continue;
7948                        }
7949
7950                        // Try MCP dispatch first, then fall back to TL function lookup
7951                        let result_str;
7952                        #[cfg(feature = "mcp")]
7953                        {
7954                            if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
7955                                let mcp_result = mcp_clients[client_idx]
7956                                    .call_tool(&tc.name, tc.input.clone())
7957                                    .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
7958                                result_str = mcp_result
7959                                    .content
7960                                    .iter()
7961                                    .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
7962                                    .collect::<Vec<_>>()
7963                                    .join("\n");
7964                            } else {
7965                                result_str = self.execute_tool_call(&tc.name, &tc.input)?;
7966                            }
7967                        }
7968                        #[cfg(not(feature = "mcp"))]
7969                        {
7970                            result_str = self.execute_tool_call(&tc.name, &tc.input)?;
7971                        }
7972
7973                        // Call on_tool_call lifecycle hook if defined
7974                        let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
7975                        if let Some(hook) = self.globals.get(&hook_name).cloned() {
7976                            let hook_args = vec![
7977                                VmValue::String(Arc::from(tc.name.as_str())),
7978                                self.json_value_to_vm(&tc.input),
7979                                VmValue::String(Arc::from(result_str.as_str())),
7980                            ];
7981                            let _ = self.call_value(hook, &hook_args);
7982                        }
7983
7984                        results.push((tc.name.clone(), result_str));
7985                    }
7986
7987                    // Format tool results and add to messages
7988                    let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
7989                    messages.extend(result_msgs);
7990                }
7991            }
7992        }
7993
7994        Err(runtime_err(format!(
7995            "Agent '{}' exceeded max_turns ({})",
7996            agent_def.name, agent_def.max_turns
7997        )))
7998    }
7999
8000    #[cfg(feature = "native")]
8001    fn execute_tool_call(
8002        &mut self,
8003        tool_name: &str,
8004        input: &serde_json::Value,
8005    ) -> Result<String, TlError> {
8006        // Look up the tool function in globals
8007        let func = self
8008            .globals
8009            .get(tool_name)
8010            .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8011            .clone();
8012
8013        // Convert JSON args to VmValues
8014        let args = self.json_to_vm_args(input);
8015
8016        // Call the function using call_value
8017        let result = self.call_value(func, &args)?;
8018
8019        // Convert result to string for the LLM
8020        Ok(format!("{result}"))
8021    }
8022
8023    #[cfg(feature = "native")]
8024    fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8025        match input {
8026            serde_json::Value::Object(map) => {
8027                // Pass values in order as positional args
8028                map.values().map(|v| self.json_value_to_vm(v)).collect()
8029            }
8030            serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8031            _ => vec![self.json_value_to_vm(input)],
8032        }
8033    }
8034
8035    #[cfg(feature = "native")]
8036    fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8037        match val {
8038            serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8039            serde_json::Value::Number(n) => {
8040                if let Some(i) = n.as_i64() {
8041                    VmValue::Int(i)
8042                } else if let Some(f) = n.as_f64() {
8043                    VmValue::Float(f)
8044                } else {
8045                    VmValue::None
8046                }
8047            }
8048            serde_json::Value::Bool(b) => VmValue::Bool(*b),
8049            serde_json::Value::Null => VmValue::None,
8050            serde_json::Value::Array(arr) => VmValue::List(Box::new(
8051                arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8052            )),
8053            serde_json::Value::Object(map) => {
8054                let pairs: Vec<(Arc<str>, VmValue)> = map
8055                    .iter()
8056                    .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8057                    .collect();
8058                VmValue::Map(Box::new(pairs))
8059            }
8060        }
8061    }
8062
8063    #[cfg(feature = "native")]
8064    fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8065        match &func {
8066            VmValue::Function(_) => {
8067                // Set up a synthetic call: push args to stack, do_call
8068                let save_len = self.stack.len();
8069                let func_slot = save_len;
8070                let _args_start = func_slot + 1;
8071                self.stack.push(func.clone());
8072                for arg in args {
8073                    self.stack.push(arg.clone());
8074                }
8075                self.ensure_stack(self.stack.len() + 256);
8076
8077                self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8078
8079                // Run until the function returns
8080                let entry_depth = self.frames.len() - 1;
8081                while self.frames.len() > entry_depth {
8082                    if self.run_step(entry_depth)?.is_some() {
8083                        break;
8084                    }
8085                }
8086
8087                // Result is at func_slot
8088                let result = self.stack[func_slot].clone();
8089                self.stack.truncate(save_len);
8090                Ok(result)
8091            }
8092            VmValue::Builtin(id) => {
8093                let id_u16 = *id as u16;
8094                let save_len = self.stack.len();
8095                for arg in args {
8096                    self.stack.push(arg.clone());
8097                }
8098                let result = self.call_builtin(id_u16, save_len, args.len())?;
8099                self.stack.truncate(save_len);
8100                Ok(result)
8101            }
8102            _ => Err(runtime_err(format!(
8103                "Agent tool '{}' is not callable",
8104                func.type_name()
8105            ))),
8106        }
8107    }
8108
8109    #[cfg(feature = "native")]
8110    fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8111        if let Some(dur) = s.strip_prefix("tumbling:") {
8112            let ms = tl_stream::parse_duration(dur).ok()?;
8113            Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8114        } else if let Some(rest) = s.strip_prefix("sliding:") {
8115            let parts: Vec<&str> = rest.splitn(2, ':').collect();
8116            if parts.len() == 2 {
8117                let wms = tl_stream::parse_duration(parts[0]).ok()?;
8118                let sms = tl_stream::parse_duration(parts[1]).ok()?;
8119                Some(tl_stream::window::WindowType::Sliding {
8120                    window_ms: wms,
8121                    slide_ms: sms,
8122                })
8123            } else {
8124                None
8125            }
8126        } else if let Some(dur) = s.strip_prefix("session:") {
8127            let ms = tl_stream::parse_duration(dur).ok()?;
8128            Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8129        } else {
8130            None
8131        }
8132    }
8133
8134    #[cfg(feature = "native")]
8135    fn handle_connector_decl(
8136        &mut self,
8137        frame_idx: usize,
8138        type_const: u8,
8139        config_const: u8,
8140    ) -> Result<VmValue, TlError> {
8141        let frame = &self.frames[frame_idx];
8142        let connector_type = match &frame.prototype.constants[type_const as usize] {
8143            Constant::String(s) => s.to_string(),
8144            _ => return Err(runtime_err("Expected string constant for connector type")),
8145        };
8146
8147        let config_args = match &frame.prototype.constants[config_const as usize] {
8148            Constant::AstExprList(args) => args.clone(),
8149            _ => return Err(runtime_err("Expected AST expr list for connector config")),
8150        };
8151
8152        let mut properties = std::collections::HashMap::new();
8153        for arg in &config_args {
8154            if let AstExpr::NamedArg { name: key, value } = arg {
8155                let val_str = match value.as_ref() {
8156                    AstExpr::String(s) => s.clone(),
8157                    AstExpr::Int(n) => n.to_string(),
8158                    AstExpr::Float(f) => f.to_string(),
8159                    AstExpr::Bool(b) => b.to_string(),
8160                    other => {
8161                        // Try to resolve Ident from globals
8162                        if let AstExpr::Ident(ident) = other {
8163                            if let Some(val) = self.globals.get(ident.as_str()) {
8164                                format!("{val}")
8165                            } else {
8166                                ident.clone()
8167                            }
8168                        } else {
8169                            format!("{other:?}")
8170                        }
8171                    }
8172                };
8173                properties.insert(key.clone(), val_str);
8174            }
8175        }
8176
8177        let config = tl_stream::ConnectorConfig {
8178            name: String::new(), // Will be set by SetGlobal
8179            connector_type,
8180            properties,
8181        };
8182
8183        Ok(VmValue::Connector(Arc::new(config)))
8184    }
8185
8186    /// Advance a generator by one step, returning the next value or None if done.
8187    fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8188        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8189        if gn.done {
8190            return Ok(VmValue::None);
8191        }
8192        match &mut gn.kind {
8193            GeneratorKind::UserDefined {
8194                prototype,
8195                upvalues,
8196                saved_stack,
8197                ip,
8198            } => {
8199                let proto = prototype.clone();
8200                let uvs = upvalues.clone();
8201                let stack_snapshot = saved_stack.clone();
8202                let saved_ip = *ip;
8203                drop(gn); // release lock before running VM
8204
8205                // Set up a frame to resume the generator
8206                let new_base = self.stack.len();
8207                let num_regs = proto.num_registers as usize;
8208                self.ensure_stack(new_base + num_regs + 1);
8209                // Restore saved registers
8210                for (i, val) in stack_snapshot.iter().enumerate() {
8211                    self.stack[new_base + i] = val.clone();
8212                }
8213
8214                self.frames.push(CallFrame {
8215                    prototype: proto,
8216                    ip: saved_ip,
8217                    base: new_base,
8218                    upvalues: uvs,
8219                });
8220
8221                self.yielded_value = None;
8222                let _result = self.run()?;
8223
8224                if let Some(yielded) = self.yielded_value.take() {
8225                    // Generator yielded — save state back
8226                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8227                    if let GeneratorKind::UserDefined {
8228                        saved_stack, ip, ..
8229                    } = &mut gn.kind
8230                    {
8231                        // Save the current register state
8232                        let num_regs_save = saved_stack.len();
8233                        for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8234                            if new_base + i < self.stack.len() {
8235                                *slot = self.stack[new_base + i].clone();
8236                            }
8237                        }
8238                        // Save the ip (instruction after yield)
8239                        *ip = self.yielded_ip;
8240                    }
8241                    self.stack.truncate(new_base);
8242                    Ok(yielded)
8243                } else {
8244                    // Generator returned (no yield) — mark done
8245                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8246                    gn.done = true;
8247                    self.stack.truncate(new_base);
8248                    Ok(VmValue::None)
8249                }
8250            }
8251            GeneratorKind::ListIter { items, index } => {
8252                if *index < items.len() {
8253                    let val = items[*index].clone();
8254                    *index += 1;
8255                    Ok(val)
8256                } else {
8257                    gn.done = true;
8258                    Ok(VmValue::None)
8259                }
8260            }
8261            GeneratorKind::Take { source, remaining } => {
8262                if *remaining == 0 {
8263                    gn.done = true;
8264                    return Ok(VmValue::None);
8265                }
8266                *remaining -= 1;
8267                let src = source.clone();
8268                drop(gn);
8269                let val = self.generator_next(&src)?;
8270                if matches!(val, VmValue::None) {
8271                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8272                    gn.done = true;
8273                }
8274                Ok(val)
8275            }
8276            GeneratorKind::Skip { source, remaining } => {
8277                let src = source.clone();
8278                let skip_n = *remaining;
8279                *remaining = 0;
8280                drop(gn);
8281                // Skip initial values
8282                for _ in 0..skip_n {
8283                    let val = self.generator_next(&src)?;
8284                    if matches!(val, VmValue::None) {
8285                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8286                        gn.done = true;
8287                        return Ok(VmValue::None);
8288                    }
8289                }
8290                let val = self.generator_next(&src)?;
8291                if matches!(val, VmValue::None) {
8292                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8293                    gn.done = true;
8294                }
8295                Ok(val)
8296            }
8297            GeneratorKind::Map { source, func } => {
8298                let src = source.clone();
8299                let f = func.clone();
8300                drop(gn);
8301                let val = self.generator_next(&src)?;
8302                if matches!(val, VmValue::None) {
8303                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8304                    gn.done = true;
8305                    return Ok(VmValue::None);
8306                }
8307                self.call_vm_function(&f, &[val])
8308            }
8309            GeneratorKind::Filter { source, func } => {
8310                let src = source.clone();
8311                let f = func.clone();
8312                drop(gn);
8313                loop {
8314                    let val = self.generator_next(&src)?;
8315                    if matches!(val, VmValue::None) {
8316                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8317                        gn.done = true;
8318                        return Ok(VmValue::None);
8319                    }
8320                    let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8321                    if test.is_truthy() {
8322                        return Ok(val);
8323                    }
8324                }
8325            }
8326            GeneratorKind::Chain {
8327                first,
8328                second,
8329                on_second,
8330            } => {
8331                if !*on_second {
8332                    let src = first.clone();
8333                    drop(gn);
8334                    let val = self.generator_next(&src)?;
8335                    if matches!(val, VmValue::None) {
8336                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8337                        if let GeneratorKind::Chain {
8338                            on_second, second, ..
8339                        } = &mut gn.kind
8340                        {
8341                            *on_second = true;
8342                            let src2 = second.clone();
8343                            drop(gn);
8344                            return self.generator_next(&src2);
8345                        }
8346                    }
8347                    Ok(val)
8348                } else {
8349                    let src = second.clone();
8350                    drop(gn);
8351                    let val = self.generator_next(&src)?;
8352                    if matches!(val, VmValue::None) {
8353                        let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8354                        gn.done = true;
8355                    }
8356                    Ok(val)
8357                }
8358            }
8359            GeneratorKind::Zip { first, second } => {
8360                let src1 = first.clone();
8361                let src2 = second.clone();
8362                drop(gn);
8363                let val1 = self.generator_next(&src1)?;
8364                let val2 = self.generator_next(&src2)?;
8365                if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8366                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8367                    gn.done = true;
8368                    return Ok(VmValue::None);
8369                }
8370                Ok(VmValue::List(Box::new(vec![val1, val2])))
8371            }
8372            GeneratorKind::Enumerate { source, index } => {
8373                let src = source.clone();
8374                let idx = *index;
8375                *index += 1;
8376                drop(gn);
8377                let val = self.generator_next(&src)?;
8378                if matches!(val, VmValue::None) {
8379                    let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8380                    gn.done = true;
8381                    return Ok(VmValue::None);
8382                }
8383                Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8384            }
8385        }
8386    }
8387
8388    /// Process a __schema__:Name:vN:fields... global to register in schema_registry.
8389    #[cfg(feature = "native")]
8390    fn process_schema_global(&mut self, s: &str) {
8391        // Format: __schema__:Name:vN:field1:Type,field2:Type,...
8392        let rest = &s["__schema__:".len()..];
8393        let parts: Vec<&str> = rest.splitn(3, ':').collect();
8394        if parts.len() < 2 {
8395            return;
8396        }
8397
8398        let schema_name = parts[0];
8399        let mut version: i64 = 0;
8400        let fields_str;
8401
8402        if parts.len() == 3 && parts[1].starts_with('v') {
8403            // Versioned: Name:vN:fields
8404            version = parts[1][1..].parse().unwrap_or(0);
8405            fields_str = parts[2];
8406        } else if parts.len() == 3 {
8407            // No version prefix, treat as v0: Name:field1:...
8408            fields_str = &rest[schema_name.len() + 1..];
8409        } else {
8410            fields_str = parts[1];
8411        }
8412
8413        if version == 0 {
8414            return;
8415        } // Only register versioned schemas
8416
8417        let mut arrow_fields = Vec::new();
8418        for field_pair in fields_str.split(',') {
8419            let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8420            if kv.len() == 2 {
8421                let fname = kv[0].trim();
8422                let ftype = kv[1].trim();
8423                // Parse type expr debug format: Simple("typename")
8424                let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8425                    &ftype[8..ftype.len() - 2]
8426                } else {
8427                    ftype
8428                };
8429                let dt = crate::schema::type_name_to_arrow_pub(type_name);
8430                arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8431            }
8432        }
8433
8434        if !arrow_fields.is_empty() {
8435            let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8436            let _ = self.schema_registry.register(
8437                schema_name,
8438                version,
8439                schema,
8440                crate::schema::SchemaMetadata::default(),
8441            );
8442        }
8443    }
8444
8445    /// Process a __migrate__:Name:fromVer:toVer:ops global to apply migration.
8446    #[cfg(feature = "native")]
8447    fn process_migrate_global(&mut self, s: &str) {
8448        // Format: __migrate__:Name:from:to:op1;op2;...
8449        let rest = &s["__migrate__:".len()..];
8450        let parts: Vec<&str> = rest.splitn(4, ':').collect();
8451        if parts.len() < 4 {
8452            return;
8453        }
8454
8455        let schema_name = parts[0];
8456        let from_ver: i64 = parts[1].parse().unwrap_or(0);
8457        let to_ver: i64 = parts[2].parse().unwrap_or(0);
8458        let ops_str = parts[3];
8459
8460        let mut ops = Vec::new();
8461        for op_str in ops_str.split(';') {
8462            let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8463            if op_parts.is_empty() {
8464                continue;
8465            }
8466            match op_parts[0] {
8467                "add" if op_parts.len() >= 3 => {
8468                    let name = op_parts[1].to_string();
8469                    // Parse type from debug format: Simple("typename")
8470                    let type_raw = op_parts[2];
8471                    let type_name =
8472                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8473                            type_raw[8..type_raw.len() - 2].to_string()
8474                        } else {
8475                            type_raw.to_string()
8476                        };
8477                    let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
8478                        Some(
8479                            op_parts[3]["default:".len()..]
8480                                .trim_matches('"')
8481                                .to_string(),
8482                        )
8483                    } else {
8484                        None
8485                    };
8486                    ops.push(crate::schema::MigrationOp::AddColumn {
8487                        name,
8488                        type_name,
8489                        default,
8490                    });
8491                }
8492                "drop" if op_parts.len() >= 2 => {
8493                    ops.push(crate::schema::MigrationOp::DropColumn {
8494                        name: op_parts[1].to_string(),
8495                    });
8496                }
8497                "rename" if op_parts.len() >= 3 => {
8498                    ops.push(crate::schema::MigrationOp::RenameColumn {
8499                        from: op_parts[1].to_string(),
8500                        to: op_parts[2].to_string(),
8501                    });
8502                }
8503                "alter" if op_parts.len() >= 3 => {
8504                    let type_raw = op_parts[2];
8505                    let type_name =
8506                        if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
8507                            type_raw[8..type_raw.len() - 2].to_string()
8508                        } else {
8509                            type_raw.to_string()
8510                        };
8511                    ops.push(crate::schema::MigrationOp::AlterType {
8512                        column: op_parts[1].to_string(),
8513                        new_type: type_name,
8514                    });
8515                }
8516                _ => {}
8517            }
8518        }
8519
8520        let _ = self
8521            .schema_registry
8522            .apply_migration(schema_name, from_ver, to_ver, &ops);
8523    }
8524
8525    /// Dispatch a method call on an object.
8526    /// Deep-clone a VmValue, recursively copying containers.
8527    fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
8528        match val {
8529            VmValue::List(items) => {
8530                let cloned: Result<Vec<_>, _> =
8531                    items.iter().map(|v| self.deep_clone_value(v)).collect();
8532                Ok(VmValue::List(Box::new(cloned?)))
8533            }
8534            VmValue::Map(pairs) => {
8535                let cloned: Result<Vec<_>, _> = pairs
8536                    .iter()
8537                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8538                    .collect();
8539                Ok(VmValue::Map(Box::new(cloned?)))
8540            }
8541            VmValue::Set(items) => {
8542                let cloned: Result<Vec<_>, _> =
8543                    items.iter().map(|v| self.deep_clone_value(v)).collect();
8544                Ok(VmValue::Set(Box::new(cloned?)))
8545            }
8546            VmValue::StructInstance(inst) => {
8547                let cloned_fields: Result<Vec<_>, _> = inst
8548                    .fields
8549                    .iter()
8550                    .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
8551                    .collect();
8552                Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
8553                    type_name: inst.type_name.clone(),
8554                    fields: cloned_fields?,
8555                })))
8556            }
8557            VmValue::EnumInstance(e) => {
8558                let cloned_fields: Result<Vec<_>, _> =
8559                    e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
8560                Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
8561                    type_name: e.type_name.clone(),
8562                    variant: e.variant.clone(),
8563                    fields: cloned_fields?,
8564                })))
8565            }
8566            #[cfg(feature = "gpu")]
8567            VmValue::GpuTensor(gt) => {
8568                let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
8569                Ok(VmValue::GpuTensor(Arc::new(cloned)))
8570            }
8571            VmValue::Ref(inner) => self.deep_clone_value(inner),
8572            VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
8573            VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
8574            VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
8575            VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
8576            other => Ok(other.clone()),
8577        }
8578    }
8579
8580    pub fn dispatch_method(
8581        &mut self,
8582        obj: VmValue,
8583        method: &str,
8584        args: &[VmValue],
8585    ) -> Result<VmValue, TlError> {
8586        // Universal .clone() method — deep copy any value
8587        if method == "clone" {
8588            return self.deep_clone_value(&obj);
8589        }
8590        // Unwrap Ref for method dispatch — methods can be called through references
8591        let obj = match obj {
8592            VmValue::Ref(inner) => inner.as_ref().clone(),
8593            other => other,
8594        };
8595        match &obj {
8596            VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
8597            VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
8598            VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
8599            VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
8600            VmValue::Module(m) => {
8601                if let Some(func) = m.exports.get(method).cloned() {
8602                    self.call_vm_function(&func, args)
8603                } else {
8604                    Err(runtime_err(format!(
8605                        "Module '{}' has no export '{}'",
8606                        m.name, method
8607                    )))
8608                }
8609            }
8610            VmValue::StructInstance(inst) => {
8611                // Look up impl method: Type::method in globals
8612                let mangled = format!("{}::{}", inst.type_name, method);
8613                if let Some(func) = self.globals.get(&mangled).cloned() {
8614                    let mut all_args = vec![obj.clone()];
8615                    all_args.extend_from_slice(args);
8616                    self.call_vm_function(&func, &all_args)
8617                } else {
8618                    Err(runtime_err(format!(
8619                        "No method '{}' on struct '{}'",
8620                        method, inst.type_name
8621                    )))
8622                }
8623            }
8624            #[cfg(feature = "python")]
8625            VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
8626            #[cfg(feature = "gpu")]
8627            VmValue::GpuTensor(gt) => match method {
8628                "to_cpu" => {
8629                    let cpu = gt.to_cpu().map_err(runtime_err)?;
8630                    Ok(VmValue::Tensor(Arc::new(cpu)))
8631                }
8632                "shape" => {
8633                    let shape_list = gt.shape.iter().map(|&d| VmValue::Int(d as i64)).collect();
8634                    Ok(VmValue::List(shape_list))
8635                }
8636                "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
8637                _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
8638            },
8639            _ => {
8640                // Try looking up Type::method from type_name
8641                let type_name = obj.type_name();
8642                let mangled = format!("{}::{}", type_name, method);
8643                if let Some(func) = self.globals.get(&mangled).cloned() {
8644                    let mut all_args = vec![obj];
8645                    all_args.extend_from_slice(args);
8646                    self.call_vm_function(&func, &all_args)
8647                } else {
8648                    Err(runtime_err(format!(
8649                        "No method '{}' on type '{}'",
8650                        method, type_name
8651                    )))
8652                }
8653            }
8654        }
8655    }
8656
8657    /// Dispatch string methods.
8658    fn dispatch_string_method(
8659        &self,
8660        s: Arc<str>,
8661        method: &str,
8662        args: &[VmValue],
8663    ) -> Result<VmValue, TlError> {
8664        match method {
8665            "len" => Ok(VmValue::Int(s.len() as i64)),
8666            "split" => {
8667                let sep = match args.first() {
8668                    Some(VmValue::String(sep)) => sep.to_string(),
8669                    _ => return Err(runtime_err("split() expects a string separator")),
8670                };
8671                let parts: Vec<VmValue> = s
8672                    .split(&sep)
8673                    .map(|p| VmValue::String(Arc::from(p)))
8674                    .collect();
8675                Ok(VmValue::List(Box::new(parts)))
8676            }
8677            "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
8678            "contains" => {
8679                let needle = match args.first() {
8680                    Some(VmValue::String(n)) => n.to_string(),
8681                    _ => return Err(runtime_err("contains() expects a string")),
8682                };
8683                Ok(VmValue::Bool(s.contains(&needle)))
8684            }
8685            "replace" => {
8686                if args.len() < 2 {
8687                    return Err(runtime_err("replace() expects 2 arguments (old, new)"));
8688                }
8689                let old = match &args[0] {
8690                    VmValue::String(s) => s.to_string(),
8691                    _ => return Err(runtime_err("replace() arg must be string")),
8692                };
8693                let new = match &args[1] {
8694                    VmValue::String(s) => s.to_string(),
8695                    _ => return Err(runtime_err("replace() arg must be string")),
8696                };
8697                Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
8698            }
8699            "starts_with" => {
8700                let prefix = match args.first() {
8701                    Some(VmValue::String(p)) => p.to_string(),
8702                    _ => return Err(runtime_err("starts_with() expects a string")),
8703                };
8704                Ok(VmValue::Bool(s.starts_with(&prefix)))
8705            }
8706            "ends_with" => {
8707                let suffix = match args.first() {
8708                    Some(VmValue::String(p)) => p.to_string(),
8709                    _ => return Err(runtime_err("ends_with() expects a string")),
8710                };
8711                Ok(VmValue::Bool(s.ends_with(&suffix)))
8712            }
8713            "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
8714            "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
8715            "chars" => {
8716                let chars: Vec<VmValue> = s
8717                    .chars()
8718                    .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
8719                    .collect();
8720                Ok(VmValue::List(Box::new(chars)))
8721            }
8722            "repeat" => {
8723                let n = match args.first() {
8724                    Some(VmValue::Int(n)) => *n as usize,
8725                    _ => return Err(runtime_err("repeat() expects an integer")),
8726                };
8727                Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
8728            }
8729            "index_of" => {
8730                let needle = match args.first() {
8731                    Some(VmValue::String(n)) => n.to_string(),
8732                    _ => return Err(runtime_err("index_of() expects a string")),
8733                };
8734                Ok(VmValue::Int(
8735                    s.find(&needle).map(|i| i as i64).unwrap_or(-1),
8736                ))
8737            }
8738            "substring" => {
8739                if args.len() < 2 {
8740                    return Err(runtime_err("substring() expects start and end"));
8741                }
8742                let start = match &args[0] {
8743                    VmValue::Int(n) => *n as usize,
8744                    _ => return Err(runtime_err("substring() expects integers")),
8745                };
8746                let end = match &args[1] {
8747                    VmValue::Int(n) => *n as usize,
8748                    _ => return Err(runtime_err("substring() expects integers")),
8749                };
8750                let end = end.min(s.len());
8751                let start = start.min(end);
8752                Ok(VmValue::String(Arc::from(&s[start..end])))
8753            }
8754            "pad_left" => {
8755                if args.is_empty() {
8756                    return Err(runtime_err("pad_left() expects width"));
8757                }
8758                let width = match &args[0] {
8759                    VmValue::Int(n) => *n as usize,
8760                    _ => return Err(runtime_err("pad_left() expects integer width")),
8761                };
8762                let ch = match args.get(1) {
8763                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8764                    _ => ' ',
8765                };
8766                if s.len() >= width {
8767                    Ok(VmValue::String(s))
8768                } else {
8769                    Ok(VmValue::String(Arc::from(
8770                        format!(
8771                            "{}{}",
8772                            std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
8773                            s
8774                        )
8775                        .as_str(),
8776                    )))
8777                }
8778            }
8779            "pad_right" => {
8780                if args.is_empty() {
8781                    return Err(runtime_err("pad_right() expects width"));
8782                }
8783                let width = match &args[0] {
8784                    VmValue::Int(n) => *n as usize,
8785                    _ => return Err(runtime_err("pad_right() expects integer width")),
8786                };
8787                let ch = match args.get(1) {
8788                    Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
8789                    _ => ' ',
8790                };
8791                if s.len() >= width {
8792                    Ok(VmValue::String(s))
8793                } else {
8794                    Ok(VmValue::String(Arc::from(
8795                        format!(
8796                            "{}{}",
8797                            s,
8798                            std::iter::repeat_n(ch, width - s.len()).collect::<String>()
8799                        )
8800                        .as_str(),
8801                    )))
8802                }
8803            }
8804            "join" => {
8805                // "sep".join(list) -> string
8806                let items = match args.first() {
8807                    Some(VmValue::List(items)) => items,
8808                    _ => return Err(runtime_err("join() expects a list")),
8809                };
8810                let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
8811                Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
8812            }
8813            "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
8814            "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
8815            "count" => {
8816                if args.is_empty() {
8817                    return Err(runtime_err("count() expects a substring"));
8818                }
8819                if let VmValue::String(sub) = &args[0] {
8820                    Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
8821                } else {
8822                    Err(runtime_err("count() expects a string"))
8823                }
8824            }
8825            "is_empty" => Ok(VmValue::Bool(s.is_empty())),
8826            "is_numeric" => Ok(VmValue::Bool(
8827                s.chars()
8828                    .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
8829            )),
8830            "is_alpha" => Ok(VmValue::Bool(
8831                !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
8832            )),
8833            "strip_prefix" => {
8834                if args.is_empty() {
8835                    return Err(runtime_err("strip_prefix() expects a string"));
8836                }
8837                if let VmValue::String(prefix) = &args[0] {
8838                    match s.strip_prefix(prefix.as_ref()) {
8839                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8840                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8841                    }
8842                } else {
8843                    Err(runtime_err("strip_prefix() expects a string"))
8844                }
8845            }
8846            "strip_suffix" => {
8847                if args.is_empty() {
8848                    return Err(runtime_err("strip_suffix() expects a string"));
8849                }
8850                if let VmValue::String(suffix) = &args[0] {
8851                    match s.strip_suffix(suffix.as_ref()) {
8852                        Some(rest) => Ok(VmValue::String(Arc::from(rest))),
8853                        None => Ok(VmValue::String(Arc::from(s.as_ref()))),
8854                    }
8855                } else {
8856                    Err(runtime_err("strip_suffix() expects a string"))
8857                }
8858            }
8859            _ => Err(runtime_err(format!("No method '{}' on string", method))),
8860        }
8861    }
8862
8863    /// Dispatch list methods.
8864    fn dispatch_list_method(
8865        &mut self,
8866        items: Vec<VmValue>,
8867        method: &str,
8868        args: &[VmValue],
8869    ) -> Result<VmValue, TlError> {
8870        match method {
8871            "len" => Ok(VmValue::Int(items.len() as i64)),
8872            "push" => {
8873                if args.is_empty() {
8874                    return Err(runtime_err("push() expects 1 argument"));
8875                }
8876                let mut new_items = items;
8877                new_items.push(args[0].clone());
8878                Ok(VmValue::List(Box::new(new_items)))
8879            }
8880            "map" => {
8881                if args.is_empty() {
8882                    return Err(runtime_err("map() expects a function"));
8883                }
8884                let func = &args[0];
8885                let mut result = Vec::new();
8886                for item in items {
8887                    let val = self.call_vm_function(func, &[item])?;
8888                    result.push(val);
8889                }
8890                Ok(VmValue::List(Box::new(result)))
8891            }
8892            "filter" => {
8893                if args.is_empty() {
8894                    return Err(runtime_err("filter() expects a function"));
8895                }
8896                let func = &args[0];
8897                let mut result = Vec::new();
8898                for item in items {
8899                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
8900                    if val.is_truthy() {
8901                        result.push(item);
8902                    }
8903                }
8904                Ok(VmValue::List(Box::new(result)))
8905            }
8906            "reduce" => {
8907                if args.len() < 2 {
8908                    return Err(runtime_err("reduce() expects initial value and function"));
8909                }
8910                let mut acc = args[0].clone();
8911                let func = &args[1];
8912                for item in items {
8913                    acc = self.call_vm_function(func, &[acc, item])?;
8914                }
8915                Ok(acc)
8916            }
8917            "sort" => {
8918                let mut sorted = items;
8919                sorted.sort_by(|a, b| match (a, b) {
8920                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
8921                    (VmValue::Float(x), VmValue::Float(y)) => {
8922                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
8923                    }
8924                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
8925                    _ => std::cmp::Ordering::Equal,
8926                });
8927                Ok(VmValue::List(Box::new(sorted)))
8928            }
8929            "reverse" => {
8930                let mut reversed = items;
8931                reversed.reverse();
8932                Ok(VmValue::List(Box::new(reversed)))
8933            }
8934            "contains" => {
8935                if args.is_empty() {
8936                    return Err(runtime_err("contains() expects a value"));
8937                }
8938                let needle = &args[0];
8939                let found = items.iter().any(|item| match (item, needle) {
8940                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
8941                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
8942                    (VmValue::String(a), VmValue::String(b)) => a == b,
8943                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8944                    (VmValue::None, VmValue::None) => true,
8945                    _ => false,
8946                });
8947                Ok(VmValue::Bool(found))
8948            }
8949            "index_of" => {
8950                if args.is_empty() {
8951                    return Err(runtime_err("index_of() expects a value"));
8952                }
8953                let needle = &args[0];
8954                let idx = items.iter().position(|item| match (item, needle) {
8955                    (VmValue::Int(a), VmValue::Int(b)) => a == b,
8956                    (VmValue::Float(a), VmValue::Float(b)) => a == b,
8957                    (VmValue::String(a), VmValue::String(b)) => a == b,
8958                    (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
8959                    (VmValue::None, VmValue::None) => true,
8960                    _ => false,
8961                });
8962                Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
8963            }
8964            "slice" => {
8965                if args.len() < 2 {
8966                    return Err(runtime_err("slice() expects start and end"));
8967                }
8968                let start = match &args[0] {
8969                    VmValue::Int(n) => *n as usize,
8970                    _ => return Err(runtime_err("slice() expects integers")),
8971                };
8972                let end = match &args[1] {
8973                    VmValue::Int(n) => *n as usize,
8974                    _ => return Err(runtime_err("slice() expects integers")),
8975                };
8976                let end = end.min(items.len());
8977                let start = start.min(end);
8978                Ok(VmValue::List(Box::new(items[start..end].to_vec())))
8979            }
8980            "flat_map" => {
8981                if args.is_empty() {
8982                    return Err(runtime_err("flat_map() expects a function"));
8983                }
8984                let func = &args[0];
8985                let mut result = Vec::new();
8986                for item in items {
8987                    let val = self.call_vm_function(func, &[item])?;
8988                    match val {
8989                        VmValue::List(sub) => result.extend(*sub),
8990                        other => result.push(other),
8991                    }
8992                }
8993                Ok(VmValue::List(Box::new(result)))
8994            }
8995            "find" => {
8996                if args.is_empty() {
8997                    return Err(runtime_err("find() expects a predicate function"));
8998                }
8999                let func = &args[0];
9000                for item in items {
9001                    let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9002                    if val.is_truthy() {
9003                        return Ok(item);
9004                    }
9005                }
9006                Ok(VmValue::None)
9007            }
9008            "sort_by" => {
9009                if args.is_empty() {
9010                    return Err(runtime_err("sort_by() expects a key function"));
9011                }
9012                let func = &args[0];
9013                let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9014                for item in items {
9015                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9016                    keyed.push((key, item));
9017                }
9018                keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9019                    (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9020                    (VmValue::Float(x), VmValue::Float(y)) => {
9021                        x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9022                    }
9023                    (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9024                    _ => std::cmp::Ordering::Equal,
9025                });
9026                Ok(VmValue::List(Box::new(
9027                    keyed.into_iter().map(|(_, v)| v).collect(),
9028                )))
9029            }
9030            "group_by" => {
9031                if args.is_empty() {
9032                    return Err(runtime_err("group_by() expects a key function"));
9033                }
9034                let func = &args[0];
9035                let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9036                for item in items {
9037                    let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9038                    let key_str: Arc<str> = match &key {
9039                        VmValue::String(s) => s.clone(),
9040                        other => Arc::from(format!("{other}").as_str()),
9041                    };
9042                    if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9043                        group.1.push(item);
9044                    } else {
9045                        groups.push((key_str, vec![item]));
9046                    }
9047                }
9048                let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9049                    .into_iter()
9050                    .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9051                    .collect();
9052                Ok(VmValue::Map(Box::new(map_pairs)))
9053            }
9054            "unique" => {
9055                let mut seen = Vec::new();
9056                let mut result = Vec::new();
9057                for item in &items {
9058                    let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9059                    if !is_dup {
9060                        seen.push(item.clone());
9061                        result.push(item.clone());
9062                    }
9063                }
9064                Ok(VmValue::List(Box::new(result)))
9065            }
9066            "flatten" => {
9067                let mut result = Vec::new();
9068                for item in items {
9069                    match item {
9070                        VmValue::List(sub) => result.extend(*sub),
9071                        other => result.push(other),
9072                    }
9073                }
9074                Ok(VmValue::List(Box::new(result)))
9075            }
9076            "chunk" => {
9077                if args.is_empty() {
9078                    return Err(runtime_err("chunk() expects a size"));
9079                }
9080                let n = match &args[0] {
9081                    VmValue::Int(n) if *n > 0 => *n as usize,
9082                    _ => return Err(runtime_err("chunk() expects a positive integer")),
9083                };
9084                let chunks: Vec<VmValue> = items
9085                    .chunks(n)
9086                    .map(|c| VmValue::List(Box::new(c.to_vec())))
9087                    .collect();
9088                Ok(VmValue::List(Box::new(chunks)))
9089            }
9090            "insert" => {
9091                if args.len() < 2 {
9092                    return Err(runtime_err("insert() expects index and value"));
9093                }
9094                let idx = match &args[0] {
9095                    VmValue::Int(n) => *n as usize,
9096                    _ => return Err(runtime_err("insert() expects integer index")),
9097                };
9098                let mut new_items = items;
9099                if idx > new_items.len() {
9100                    return Err(runtime_err("insert() index out of bounds"));
9101                }
9102                new_items.insert(idx, args[1].clone());
9103                Ok(VmValue::List(Box::new(new_items)))
9104            }
9105            "remove_at" => {
9106                if args.is_empty() {
9107                    return Err(runtime_err("remove_at() expects an index"));
9108                }
9109                let idx = match &args[0] {
9110                    VmValue::Int(n) => *n as usize,
9111                    _ => return Err(runtime_err("remove_at() expects integer index")),
9112                };
9113                let mut new_items = items;
9114                if idx >= new_items.len() {
9115                    return Err(runtime_err("remove_at() index out of bounds"));
9116                }
9117                let removed = new_items.remove(idx);
9118                Ok(removed)
9119            }
9120            "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9121            "sum" => {
9122                let mut int_sum: i64 = 0;
9123                let mut has_float = false;
9124                let mut float_sum: f64 = 0.0;
9125                for item in &items {
9126                    match item {
9127                        VmValue::Int(n) => {
9128                            if has_float {
9129                                float_sum += *n as f64;
9130                            } else {
9131                                int_sum += n;
9132                            }
9133                        }
9134                        VmValue::Float(f) => {
9135                            if !has_float {
9136                                has_float = true;
9137                                float_sum = int_sum as f64;
9138                            }
9139                            float_sum += f;
9140                        }
9141                        _ => return Err(runtime_err("sum() requires numeric list")),
9142                    }
9143                }
9144                if has_float {
9145                    Ok(VmValue::Float(float_sum))
9146                } else {
9147                    Ok(VmValue::Int(int_sum))
9148                }
9149            }
9150            "min" => {
9151                if items.is_empty() {
9152                    return Ok(VmValue::None);
9153                }
9154                let mut min_val = items[0].clone();
9155                for item in &items[1..] {
9156                    match (&min_val, item) {
9157                        (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9158                        (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9159                        _ => {}
9160                    }
9161                }
9162                Ok(min_val)
9163            }
9164            "max" => {
9165                if items.is_empty() {
9166                    return Ok(VmValue::None);
9167                }
9168                let mut max_val = items[0].clone();
9169                for item in &items[1..] {
9170                    match (&max_val, item) {
9171                        (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9172                        (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9173                        _ => {}
9174                    }
9175                }
9176                Ok(max_val)
9177            }
9178            "each" => {
9179                if args.is_empty() {
9180                    return Err(runtime_err("each() expects a function"));
9181                }
9182                let func = &args[0];
9183                for item in items {
9184                    self.call_vm_function(func, &[item])?;
9185                }
9186                Ok(VmValue::None)
9187            }
9188            "zip" => {
9189                if args.is_empty() {
9190                    return Err(runtime_err("zip() expects a list"));
9191                }
9192                let other = match &args[0] {
9193                    VmValue::List(other) => other.as_slice(),
9194                    _ => return Err(runtime_err("zip() expects a list")),
9195                };
9196                let len = items.len().min(other.len());
9197                let zipped: Vec<VmValue> = items[..len]
9198                    .iter()
9199                    .zip(other[..len].iter())
9200                    .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9201                    .collect();
9202                Ok(VmValue::List(Box::new(zipped)))
9203            }
9204            "join" => {
9205                let sep = match args.first() {
9206                    Some(VmValue::String(s)) => s.as_ref(),
9207                    _ => "",
9208                };
9209                let s: String = items
9210                    .iter()
9211                    .map(|v| format!("{v}"))
9212                    .collect::<Vec<_>>()
9213                    .join(sep);
9214                Ok(VmValue::String(Arc::from(s.as_str())))
9215            }
9216            _ => Err(runtime_err(format!("No method '{}' on list", method))),
9217        }
9218    }
9219
9220    /// Dispatch map methods.
9221    fn dispatch_map_method(
9222        &mut self,
9223        pairs: Vec<(Arc<str>, VmValue)>,
9224        method: &str,
9225        args: &[VmValue],
9226    ) -> Result<VmValue, TlError> {
9227        match method {
9228            "len" => Ok(VmValue::Int(pairs.len() as i64)),
9229            "keys" => Ok(VmValue::List(Box::new(
9230                pairs
9231                    .iter()
9232                    .map(|(k, _)| VmValue::String(k.clone()))
9233                    .collect(),
9234            ))),
9235            "values" => Ok(VmValue::List(Box::new(
9236                pairs.iter().map(|(_, v)| v.clone()).collect(),
9237            ))),
9238            "contains_key" => {
9239                if args.is_empty() {
9240                    return Err(runtime_err("contains_key() expects a key"));
9241                }
9242                if let VmValue::String(key) = &args[0] {
9243                    Ok(VmValue::Bool(
9244                        pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9245                    ))
9246                } else {
9247                    Err(runtime_err("contains_key() expects a string key"))
9248                }
9249            }
9250            "remove" => {
9251                if args.is_empty() {
9252                    return Err(runtime_err("remove() expects a key"));
9253                }
9254                if let VmValue::String(key) = &args[0] {
9255                    let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9256                        .into_iter()
9257                        .filter(|(k, _)| k.as_ref() != key.as_ref())
9258                        .collect();
9259                    Ok(VmValue::Map(Box::new(new_pairs)))
9260                } else {
9261                    Err(runtime_err("remove() expects a string key"))
9262                }
9263            }
9264            "get" => {
9265                if args.is_empty() {
9266                    return Err(runtime_err("get() expects a key"));
9267                }
9268                if let VmValue::String(key) = &args[0] {
9269                    let default = args.get(1).cloned().unwrap_or(VmValue::None);
9270                    let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9271                    Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9272                } else {
9273                    Err(runtime_err("get() expects a string key"))
9274                }
9275            }
9276            "merge" => {
9277                if args.is_empty() {
9278                    return Err(runtime_err("merge() expects a map"));
9279                }
9280                if let VmValue::Map(other) = &args[0] {
9281                    let mut merged = pairs;
9282                    for (k, v) in other.iter() {
9283                        if let Some(existing) =
9284                            merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9285                        {
9286                            existing.1 = v.clone();
9287                        } else {
9288                            merged.push((k.clone(), v.clone()));
9289                        }
9290                    }
9291                    Ok(VmValue::Map(Box::new(merged)))
9292                } else {
9293                    Err(runtime_err("merge() expects a map"))
9294                }
9295            }
9296            "entries" => {
9297                let entries: Vec<VmValue> = pairs
9298                    .iter()
9299                    .map(|(k, v)| {
9300                        VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9301                    })
9302                    .collect();
9303                Ok(VmValue::List(Box::new(entries)))
9304            }
9305            "map_values" => {
9306                if args.is_empty() {
9307                    return Err(runtime_err("map_values() expects a function"));
9308                }
9309                let func = &args[0];
9310                let mut result = Vec::new();
9311                for (k, v) in pairs {
9312                    let new_v = self.call_vm_function(func, &[v])?;
9313                    result.push((k, new_v));
9314                }
9315                Ok(VmValue::Map(Box::new(result)))
9316            }
9317            "filter" => {
9318                if args.is_empty() {
9319                    return Err(runtime_err("filter() expects a predicate function"));
9320                }
9321                let func = &args[0];
9322                let mut result = Vec::new();
9323                for (k, v) in pairs {
9324                    let val =
9325                        self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9326                    if val.is_truthy() {
9327                        result.push((k, v));
9328                    }
9329                }
9330                Ok(VmValue::Map(Box::new(result)))
9331            }
9332            "set" => {
9333                if args.len() < 2 {
9334                    return Err(runtime_err("set() expects key and value"));
9335                }
9336                if let VmValue::String(key) = &args[0] {
9337                    let mut new_pairs = pairs;
9338                    if let Some(existing) = new_pairs
9339                        .iter_mut()
9340                        .find(|(k, _)| k.as_ref() == key.as_ref())
9341                    {
9342                        existing.1 = args[1].clone();
9343                    } else {
9344                        new_pairs.push((key.clone(), args[1].clone()));
9345                    }
9346                    Ok(VmValue::Map(Box::new(new_pairs)))
9347                } else {
9348                    Err(runtime_err("set() expects a string key"))
9349                }
9350            }
9351            "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9352            _ => Err(runtime_err(format!("No method '{}' on map", method))),
9353        }
9354    }
9355
9356    /// Dispatch set methods.
9357    fn dispatch_set_method(
9358        &self,
9359        items: Vec<VmValue>,
9360        method: &str,
9361        args: &[VmValue],
9362    ) -> Result<VmValue, TlError> {
9363        match method {
9364            "len" => Ok(VmValue::Int(items.len() as i64)),
9365            "contains" => {
9366                if args.is_empty() {
9367                    return Err(runtime_err("contains() expects a value"));
9368                }
9369                Ok(VmValue::Bool(
9370                    items.iter().any(|x| vm_values_equal(x, &args[0])),
9371                ))
9372            }
9373            "add" => {
9374                if args.is_empty() {
9375                    return Err(runtime_err("add() expects a value"));
9376                }
9377                let mut new_items = items;
9378                if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9379                    new_items.push(args[0].clone());
9380                }
9381                Ok(VmValue::Set(Box::new(new_items)))
9382            }
9383            "remove" => {
9384                if args.is_empty() {
9385                    return Err(runtime_err("remove() expects a value"));
9386                }
9387                let new_items: Vec<VmValue> = items
9388                    .into_iter()
9389                    .filter(|x| !vm_values_equal(x, &args[0]))
9390                    .collect();
9391                Ok(VmValue::Set(Box::new(new_items)))
9392            }
9393            "to_list" => Ok(VmValue::List(Box::new(items))),
9394            "union" => {
9395                if args.is_empty() {
9396                    return Err(runtime_err("union() expects a set"));
9397                }
9398                if let VmValue::Set(b) = &args[0] {
9399                    let mut result = items;
9400                    for item in b.iter() {
9401                        if !result.iter().any(|x| vm_values_equal(x, item)) {
9402                            result.push(item.clone());
9403                        }
9404                    }
9405                    Ok(VmValue::Set(Box::new(result)))
9406                } else {
9407                    Err(runtime_err("union() expects a set"))
9408                }
9409            }
9410            "intersection" => {
9411                if args.is_empty() {
9412                    return Err(runtime_err("intersection() expects a set"));
9413                }
9414                if let VmValue::Set(b) = &args[0] {
9415                    let result: Vec<VmValue> = items
9416                        .into_iter()
9417                        .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9418                        .collect();
9419                    Ok(VmValue::Set(Box::new(result)))
9420                } else {
9421                    Err(runtime_err("intersection() expects a set"))
9422                }
9423            }
9424            "difference" => {
9425                if args.is_empty() {
9426                    return Err(runtime_err("difference() expects a set"));
9427                }
9428                if let VmValue::Set(b) = &args[0] {
9429                    let result: Vec<VmValue> = items
9430                        .into_iter()
9431                        .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9432                        .collect();
9433                    Ok(VmValue::Set(Box::new(result)))
9434                } else {
9435                    Err(runtime_err("difference() expects a set"))
9436                }
9437            }
9438            _ => Err(runtime_err(format!("No method '{}' on set", method))),
9439        }
9440    }
9441
9442    /// Handle import at runtime.
9443    #[cfg(feature = "native")]
9444    fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9445        // Resolve relative path from current file
9446        let resolved = if let Some(ref base) = self.file_path {
9447            let base_dir = std::path::Path::new(base)
9448                .parent()
9449                .unwrap_or(std::path::Path::new("."));
9450            let candidate = base_dir.join(path);
9451            if candidate.exists() {
9452                candidate.to_string_lossy().to_string()
9453            } else {
9454                path.to_string()
9455            }
9456        } else {
9457            path.to_string()
9458        };
9459
9460        // Circular dependency detection
9461        if self.importing_files.contains(&resolved) {
9462            return Err(runtime_err(format!("Circular import detected: {resolved}")));
9463        }
9464
9465        // Check module cache
9466        if let Some(exports) = self.module_cache.get(&resolved) {
9467            let exports = exports.clone();
9468            return self.bind_import_exports(exports, alias);
9469        }
9470
9471        // Read, parse, compile, execute the file
9472        let source = std::fs::read_to_string(&resolved)
9473            .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
9474        let program = tl_parser::parse(&source)
9475            .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
9476        let proto = crate::compiler::compile(&program)
9477            .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
9478
9479        // Track circular imports
9480        self.importing_files.insert(resolved.clone());
9481
9482        // Execute in a fresh VM with shared globals
9483        let mut import_vm = Vm::new();
9484        import_vm.file_path = Some(resolved.clone());
9485        import_vm.globals = self.globals.clone();
9486        import_vm.importing_files = self.importing_files.clone();
9487        import_vm.module_cache = self.module_cache.clone();
9488        import_vm.package_roots = self.package_roots.clone();
9489        import_vm.project_root = self.project_root.clone();
9490        import_vm.execute(&proto)?;
9491
9492        self.importing_files.remove(&resolved);
9493
9494        // Collect exports: both globals and top-level locals from the stack
9495        let mut exports = HashMap::new();
9496
9497        // 1. New globals defined in the import
9498        for (k, v) in &import_vm.globals {
9499            if !self.globals.contains_key(k) {
9500                exports.insert(k.clone(), v.clone());
9501            }
9502        }
9503
9504        // 2. Top-level locals from the prototype (on the stack)
9505        for (name, reg) in &proto.top_level_locals {
9506            if !name.starts_with("__enum_") && !exports.contains_key(name) {
9507                let stack_idx = reg;
9508                if (*stack_idx as usize) < import_vm.stack.len() {
9509                    let val = import_vm.stack[*stack_idx as usize].clone();
9510                    if !matches!(val, VmValue::None) || name.starts_with("_") {
9511                        exports.insert(name.clone(), val);
9512                    }
9513                }
9514            }
9515        }
9516
9517        // Cache the module
9518        self.module_cache.insert(resolved, exports.clone());
9519        // Also adopt any modules the sub-VM discovered
9520        for (k, v) in import_vm.module_cache {
9521            self.module_cache.entry(k).or_insert(v);
9522        }
9523
9524        self.bind_import_exports(exports, alias)
9525    }
9526
9527    /// Bind import exports into current scope.
9528    #[cfg(feature = "native")]
9529    fn bind_import_exports(
9530        &mut self,
9531        exports: HashMap<String, VmValue>,
9532        alias: &str,
9533    ) -> Result<VmValue, TlError> {
9534        if alias.is_empty() {
9535            // Wildcard import: merge all exports into current scope
9536            for (k, v) in &exports {
9537                self.globals.insert(k.clone(), v.clone());
9538            }
9539            Ok(VmValue::None)
9540        } else {
9541            // Namespaced import
9542            let module = VmModule {
9543                name: Arc::from(alias),
9544                exports,
9545            };
9546            let module_val = VmValue::Module(Arc::new(module));
9547            self.globals.insert(alias.to_string(), module_val.clone());
9548            Ok(module_val)
9549        }
9550    }
9551
9552    /// Handle use-style imports (dot-path syntax).
9553    #[cfg(feature = "native")]
9554    fn handle_use_import(
9555        &mut self,
9556        path_str: &str,
9557        extra_a: u8,
9558        kind: u8,
9559        _frame_idx: usize,
9560    ) -> Result<VmValue, TlError> {
9561        match kind {
9562            0 => {
9563                // Single: "data.transforms.clean" → import file, bind last segment
9564                let segments: Vec<&str> = path_str.split('.').collect();
9565                let file_path = self.resolve_use_path(&segments)?;
9566                // Import the module, get exports
9567                let _last = segments.last().copied().unwrap_or("");
9568                self.handle_import(&file_path, "")?;
9569                // The wildcard import already merged everything.
9570                // But for Single, we only want the specific item.
9571                // Since handle_import merges all, that works for now.
9572                // Return none since it's a statement, not an expression.
9573                Ok(VmValue::None)
9574            }
9575            1 => {
9576                // Group: "data.transforms.{a,b}" — extract prefix before {
9577                let brace_start = path_str.find('{').unwrap_or(path_str.len());
9578                let prefix = path_str[..brace_start].trim_end_matches('.');
9579                let segments: Vec<&str> = prefix.split('.').collect();
9580                let file_path = self.resolve_use_path(&segments)?;
9581                self.handle_import(&file_path, "")?;
9582                Ok(VmValue::None)
9583            }
9584            2 => {
9585                // Wildcard: "data.transforms.*" — strip trailing .*
9586                let prefix = path_str.trim_end_matches(".*");
9587                let segments: Vec<&str> = prefix.split('.').collect();
9588                let file_path = self.resolve_use_path(&segments)?;
9589                self.handle_import(&file_path, "")?;
9590                Ok(VmValue::None)
9591            }
9592            3 => {
9593                // Aliased: path in path_str, alias in extra_a (constant index)
9594                let segments: Vec<&str> = path_str.split('.').collect();
9595                let file_path = self.resolve_use_path(&segments)?;
9596                // For aliased, we need to get the alias from the constant pool
9597                // extra_a contains the constant index of the alias string
9598                let alias_str = if let Some(frame) = self.frames.last() {
9599                    if let Some(crate::chunk::Constant::String(s)) =
9600                        frame.prototype.constants.get(extra_a as usize)
9601                    {
9602                        s.to_string()
9603                    } else {
9604                        segments.last().copied().unwrap_or("module").to_string()
9605                    }
9606                } else {
9607                    segments.last().copied().unwrap_or("module").to_string()
9608                };
9609                self.handle_import(&file_path, &alias_str)?;
9610                Ok(VmValue::None)
9611            }
9612            _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
9613        }
9614    }
9615
9616    /// Resolve dot-path segments to a file path for use statements.
9617    #[cfg(feature = "native")]
9618    fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
9619        // Reject path traversal attempts
9620        if segments.contains(&"..") {
9621            return Err(runtime_err("Import paths cannot contain '..'"));
9622        }
9623
9624        let base_dir = if let Some(ref fp) = self.file_path {
9625            std::path::Path::new(fp)
9626                .parent()
9627                .unwrap_or(std::path::Path::new("."))
9628                .to_path_buf()
9629        } else {
9630            std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
9631        };
9632
9633        let rel_path = segments.join("/");
9634
9635        // Try file module first
9636        let file_path = base_dir.join(format!("{rel_path}.tl"));
9637        if file_path.exists() {
9638            return Ok(file_path.to_string_lossy().to_string());
9639        }
9640
9641        // Try directory module
9642        let dir_path = base_dir.join(&rel_path).join("mod.tl");
9643        if dir_path.exists() {
9644            return Ok(dir_path.to_string_lossy().to_string());
9645        }
9646
9647        // If multi-segment, try parent as file module
9648        if segments.len() > 1 {
9649            let parent = &segments[..segments.len() - 1];
9650            let parent_path = parent.join("/");
9651            let parent_file = base_dir.join(format!("{parent_path}.tl"));
9652            if parent_file.exists() {
9653                return Ok(parent_file.to_string_lossy().to_string());
9654            }
9655            let parent_dir = base_dir.join(&parent_path).join("mod.tl");
9656            if parent_dir.exists() {
9657                return Ok(parent_dir.to_string_lossy().to_string());
9658            }
9659        }
9660
9661        // Package import fallback: first segment as package name
9662        // Convert underscores to hyphens (TL identifiers use _, package names use -)
9663        let pkg_name_underscore = segments[0];
9664        let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
9665        let pkg_root = self
9666            .package_roots
9667            .get(pkg_name_underscore)
9668            .or_else(|| self.package_roots.get(&pkg_name_hyphen));
9669
9670        if let Some(root) = pkg_root {
9671            let remaining = &segments[1..];
9672            if let Some(path) = resolve_package_file(root, remaining) {
9673                return Ok(path);
9674            }
9675        }
9676
9677        Err(runtime_err(format!(
9678            "Module not found: `{}`",
9679            segments.join(".")
9680        )))
9681    }
9682
9683    /// Call a VmValue function/closure with args.
9684    fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
9685        match func {
9686            VmValue::Function(closure) => {
9687                let proto = closure.prototype.clone();
9688                let arity = proto.arity as usize;
9689                if args.len() != arity {
9690                    return Err(runtime_err(format!(
9691                        "Expected {} arguments, got {}",
9692                        arity,
9693                        args.len()
9694                    )));
9695                }
9696
9697                // If this is a generator function, create a Generator
9698                if proto.is_generator {
9699                    let mut closed_upvalues = Vec::new();
9700                    for uv in &closure.upvalues {
9701                        match uv {
9702                            UpvalueRef::Open { stack_index } => {
9703                                let val = self.stack[*stack_index].clone();
9704                                closed_upvalues.push(UpvalueRef::Closed(val));
9705                            }
9706                            UpvalueRef::Closed(v) => {
9707                                closed_upvalues.push(UpvalueRef::Closed(v.clone()));
9708                            }
9709                        }
9710                    }
9711                    let num_regs = proto.num_registers as usize;
9712                    let mut saved_stack = vec![VmValue::None; num_regs];
9713                    for (i, arg) in args.iter().enumerate() {
9714                        saved_stack[i] = arg.clone();
9715                    }
9716                    let gn = VmGenerator::new(GeneratorKind::UserDefined {
9717                        prototype: proto,
9718                        upvalues: closed_upvalues,
9719                        saved_stack,
9720                        ip: 0,
9721                    });
9722                    return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
9723                }
9724
9725                let new_base = self.stack.len();
9726                self.ensure_stack(new_base + proto.num_registers as usize + 1);
9727
9728                for (i, arg) in args.iter().enumerate() {
9729                    self.stack[new_base + i] = arg.clone();
9730                }
9731
9732                self.frames.push(CallFrame {
9733                    prototype: proto,
9734                    ip: 0,
9735                    base: new_base,
9736                    upvalues: closure.upvalues.clone(),
9737                });
9738
9739                let result = self.run()?;
9740                self.stack.truncate(new_base);
9741                Ok(result)
9742            }
9743            VmValue::Builtin(id) => {
9744                // Put args on stack temporarily
9745                let args_base = self.stack.len();
9746                for arg in args {
9747                    self.stack.push(arg.clone());
9748                }
9749                let result = self.call_builtin(*id as u16, args_base, args.len());
9750                self.stack.truncate(args_base);
9751                result
9752            }
9753            _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
9754        }
9755    }
9756
9757    // ── Table pipe handler ──
9758
9759    #[cfg(feature = "native")]
9760    fn handle_table_pipe(
9761        &mut self,
9762        frame_idx: usize,
9763        table_val: VmValue,
9764        op_const: u8,
9765        args_const: u8,
9766    ) -> Result<VmValue, TlError> {
9767        let df = match table_val {
9768            VmValue::Table(t) => t.df,
9769            other => {
9770                // Not a table — fall back to regular builtin/function call
9771                return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
9772            }
9773        };
9774
9775        let frame = &self.frames[frame_idx];
9776        let op_name = match &frame.prototype.constants[op_const as usize] {
9777            Constant::String(s) => s.to_string(),
9778            _ => return Err(runtime_err("Expected string constant for table op")),
9779        };
9780        let ast_args = match &frame.prototype.constants[args_const as usize] {
9781            Constant::AstExprList(args) => args.clone(),
9782            _ => return Err(runtime_err("Expected AST expr list for table args")),
9783        };
9784
9785        let ctx = self.build_translate_context();
9786
9787        match op_name.as_str() {
9788            "filter" => {
9789                if ast_args.len() != 1 {
9790                    return Err(runtime_err("filter() expects 1 argument (predicate)"));
9791                }
9792                let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
9793                let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
9794                Ok(VmValue::Table(VmTable { df: filtered }))
9795            }
9796            "select" => {
9797                if ast_args.is_empty() {
9798                    return Err(runtime_err("select() expects at least 1 argument"));
9799                }
9800                let mut select_exprs = Vec::new();
9801                for arg in &ast_args {
9802                    match arg {
9803                        AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
9804                        AstExpr::NamedArg { name, value } => {
9805                            let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9806                            select_exprs.push(expr.alias(name));
9807                        }
9808                        AstExpr::String(name) => select_exprs.push(col(name.as_str())),
9809                        other => {
9810                            let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9811                            select_exprs.push(expr);
9812                        }
9813                    }
9814                }
9815                let selected = df
9816                    .select(select_exprs)
9817                    .map_err(|e| runtime_err(format!("{e}")))?;
9818                Ok(VmValue::Table(VmTable { df: selected }))
9819            }
9820            "sort" => {
9821                if ast_args.is_empty() {
9822                    return Err(runtime_err("sort() expects at least 1 argument (column)"));
9823                }
9824                let mut sort_exprs = Vec::new();
9825                let mut i = 0;
9826                while i < ast_args.len() {
9827                    let col_name = match &ast_args[i] {
9828                        AstExpr::Ident(name) => name.clone(),
9829                        AstExpr::String(name) => name.clone(),
9830                        _ => {
9831                            return Err(runtime_err(
9832                                "sort() column must be an identifier or string",
9833                            ));
9834                        }
9835                    };
9836                    i += 1;
9837                    let ascending = if i < ast_args.len() {
9838                        match &ast_args[i] {
9839                            AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
9840                                i += 1;
9841                                false
9842                            }
9843                            AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
9844                                i += 1;
9845                                true
9846                            }
9847                            _ => true,
9848                        }
9849                    } else {
9850                        true
9851                    };
9852                    sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
9853                }
9854                let sorted = df
9855                    .sort(sort_exprs)
9856                    .map_err(|e| runtime_err(format!("{e}")))?;
9857                Ok(VmValue::Table(VmTable { df: sorted }))
9858            }
9859            "with" => {
9860                if ast_args.len() != 1 {
9861                    return Err(runtime_err(
9862                        "with() expects 1 argument (map of column definitions)",
9863                    ));
9864                }
9865                let pairs = match &ast_args[0] {
9866                    AstExpr::Map(pairs) => pairs,
9867                    _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
9868                };
9869                let mut result_df = df;
9870                for (key, value_expr) in pairs {
9871                    let col_name = match key {
9872                        AstExpr::String(s) => s.clone(),
9873                        AstExpr::Ident(s) => s.clone(),
9874                        _ => return Err(runtime_err("with() key must be a string or identifier")),
9875                    };
9876                    let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
9877                    result_df = result_df
9878                        .with_column(&col_name, df_expr)
9879                        .map_err(|e| runtime_err(format!("{e}")))?;
9880                }
9881                Ok(VmValue::Table(VmTable { df: result_df }))
9882            }
9883            "aggregate" => {
9884                let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9885                let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
9886                for arg in &ast_args {
9887                    match arg {
9888                        AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
9889                            AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
9890                            AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
9891                            AstExpr::List(items) => {
9892                                for item in items {
9893                                    match item {
9894                                        AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
9895                                        AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
9896                                        _ => {
9897                                            return Err(runtime_err(
9898                                                "by: list items must be strings or identifiers",
9899                                            ));
9900                                        }
9901                                    }
9902                                }
9903                            }
9904                            _ => return Err(runtime_err("by: must be a column name or list")),
9905                        },
9906                        AstExpr::NamedArg { name, value } => {
9907                            let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
9908                            agg_exprs.push(agg_expr.alias(name));
9909                        }
9910                        other => {
9911                            let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
9912                            agg_exprs.push(agg_expr);
9913                        }
9914                    }
9915                }
9916                let aggregated = df
9917                    .aggregate(group_by_cols, agg_exprs)
9918                    .map_err(|e| runtime_err(format!("{e}")))?;
9919                Ok(VmValue::Table(VmTable { df: aggregated }))
9920            }
9921            "join" => {
9922                if ast_args.is_empty() {
9923                    return Err(runtime_err(
9924                        "join() expects at least 1 argument (right table)",
9925                    ));
9926                }
9927                // Evaluate first arg to get right table
9928                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
9929                let right_df = match right_table {
9930                    VmValue::Table(t) => t.df,
9931                    _ => return Err(runtime_err("join() first arg must be a table")),
9932                };
9933                let mut left_cols: Vec<String> = Vec::new();
9934                let mut right_cols: Vec<String> = Vec::new();
9935                let mut join_type = JoinType::Inner;
9936                for arg in &ast_args[1..] {
9937                    match arg {
9938                        AstExpr::NamedArg { name, value } if name == "on" => {
9939                            if let AstExpr::BinOp {
9940                                left,
9941                                op: tl_ast::BinOp::Eq,
9942                                right,
9943                            } = value.as_ref()
9944                            {
9945                                let lc = match left.as_ref() {
9946                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9947                                    _ => {
9948                                        return Err(runtime_err(
9949                                            "on: left side must be a column name",
9950                                        ));
9951                                    }
9952                                };
9953                                let rc = match right.as_ref() {
9954                                    AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
9955                                    _ => {
9956                                        return Err(runtime_err(
9957                                            "on: right side must be a column name",
9958                                        ));
9959                                    }
9960                                };
9961                                left_cols.push(lc);
9962                                right_cols.push(rc);
9963                            }
9964                        }
9965                        AstExpr::NamedArg { name, value } if name == "kind" => {
9966                            if let AstExpr::String(kind_str) = value.as_ref() {
9967                                join_type = match kind_str.as_str() {
9968                                    "inner" => JoinType::Inner,
9969                                    "left" => JoinType::Left,
9970                                    "right" => JoinType::Right,
9971                                    "full" => JoinType::Full,
9972                                    _ => {
9973                                        return Err(runtime_err(format!(
9974                                            "Unknown join type: {kind_str}"
9975                                        )));
9976                                    }
9977                                };
9978                            }
9979                        }
9980                        _ => {}
9981                    }
9982                }
9983                let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
9984                let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
9985                let joined = df
9986                    .join(right_df, join_type, &lc_refs, &rc_refs, None)
9987                    .map_err(|e| runtime_err(format!("{e}")))?;
9988                Ok(VmValue::Table(VmTable { df: joined }))
9989            }
9990            "head" | "limit" => {
9991                let n = match ast_args.first() {
9992                    Some(AstExpr::Int(n)) => *n as usize,
9993                    None => 10,
9994                    _ => return Err(runtime_err("head/limit expects an integer")),
9995                };
9996                let limited = df
9997                    .limit(0, Some(n))
9998                    .map_err(|e| runtime_err(format!("{e}")))?;
9999                Ok(VmValue::Table(VmTable { df: limited }))
10000            }
10001            "collect" => {
10002                let batches = self.engine().collect(df).map_err(runtime_err)?;
10003                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10004                Ok(VmValue::String(Arc::from(formatted.as_str())))
10005            }
10006            "show" => {
10007                let limit = match ast_args.first() {
10008                    Some(AstExpr::Int(n)) => *n as usize,
10009                    None => 20,
10010                    _ => 20,
10011                };
10012                let limited = df
10013                    .limit(0, Some(limit))
10014                    .map_err(|e| runtime_err(format!("{e}")))?;
10015                let batches = self.engine().collect(limited).map_err(runtime_err)?;
10016                let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10017                println!("{formatted}");
10018                self.output.push(formatted);
10019                Ok(VmValue::None)
10020            }
10021            "describe" => {
10022                let schema = df.schema();
10023                let mut lines = Vec::new();
10024                lines.push("Columns:".to_string());
10025                for field in schema.fields() {
10026                    lines.push(format!("  {}: {}", field.name(), field.data_type()));
10027                }
10028                let output = lines.join("\n");
10029                println!("{output}");
10030                self.output.push(output.clone());
10031                Ok(VmValue::String(Arc::from(output.as_str())))
10032            }
10033            "write_csv" => {
10034                if ast_args.len() != 1 {
10035                    return Err(runtime_err("write_csv() expects 1 argument (path)"));
10036                }
10037                let path = self.eval_ast_to_string(&ast_args[0])?;
10038                self.engine().write_csv(df, &path).map_err(runtime_err)?;
10039                Ok(VmValue::None)
10040            }
10041            "write_parquet" => {
10042                if ast_args.len() != 1 {
10043                    return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10044                }
10045                let path = self.eval_ast_to_string(&ast_args[0])?;
10046                self.engine()
10047                    .write_parquet(df, &path)
10048                    .map_err(runtime_err)?;
10049                Ok(VmValue::None)
10050            }
10051            // Phase 15: Data quality pipe operations
10052            "fill_null" => {
10053                if ast_args.is_empty() {
10054                    return Err(runtime_err(
10055                        "fill_null() expects (column, [strategy/value])",
10056                    ));
10057                }
10058                let column = self.eval_ast_to_string(&ast_args[0])?;
10059                if ast_args.len() >= 2 {
10060                    let val = self.eval_ast_to_vm(&ast_args[1])?;
10061                    match val {
10062                        VmValue::String(s) => {
10063                            // String means strategy name
10064                            let fill_val = if ast_args.len() >= 3 {
10065                                match self.eval_ast_to_vm(&ast_args[2])? {
10066                                    VmValue::Int(n) => Some(n as f64),
10067                                    VmValue::Float(f) => Some(f),
10068                                    _ => None,
10069                                }
10070                            } else {
10071                                None
10072                            };
10073                            let result = self
10074                                .engine()
10075                                .fill_null(df, &column, &s, fill_val)
10076                                .map_err(runtime_err)?;
10077                            Ok(VmValue::Table(VmTable { df: result }))
10078                        }
10079                        VmValue::Int(n) => {
10080                            let result = self
10081                                .engine()
10082                                .fill_null(df, &column, "value", Some(n as f64))
10083                                .map_err(runtime_err)?;
10084                            Ok(VmValue::Table(VmTable { df: result }))
10085                        }
10086                        VmValue::Float(f) => {
10087                            let result = self
10088                                .engine()
10089                                .fill_null(df, &column, "value", Some(f))
10090                                .map_err(runtime_err)?;
10091                            Ok(VmValue::Table(VmTable { df: result }))
10092                        }
10093                        _ => Err(runtime_err(
10094                            "fill_null() second arg must be a strategy or fill value",
10095                        )),
10096                    }
10097                } else {
10098                    let result = self
10099                        .engine()
10100                        .fill_null(df, &column, "zero", None)
10101                        .map_err(runtime_err)?;
10102                    Ok(VmValue::Table(VmTable { df: result }))
10103                }
10104            }
10105            "drop_null" => {
10106                if ast_args.is_empty() {
10107                    return Err(runtime_err("drop_null() expects (column)"));
10108                }
10109                let column = self.eval_ast_to_string(&ast_args[0])?;
10110                let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10111                Ok(VmValue::Table(VmTable { df: result }))
10112            }
10113            "dedup" => {
10114                let columns: Vec<String> = ast_args
10115                    .iter()
10116                    .filter_map(|a| self.eval_ast_to_string(a).ok())
10117                    .collect();
10118                let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10119                Ok(VmValue::Table(VmTable { df: result }))
10120            }
10121            "clamp" => {
10122                if ast_args.len() < 3 {
10123                    return Err(runtime_err("clamp() expects (column, min, max)"));
10124                }
10125                let column = self.eval_ast_to_string(&ast_args[0])?;
10126                let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10127                    VmValue::Int(n) => n as f64,
10128                    VmValue::Float(f) => f,
10129                    _ => return Err(runtime_err("clamp() min must be a number")),
10130                };
10131                let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10132                    VmValue::Int(n) => n as f64,
10133                    VmValue::Float(f) => f,
10134                    _ => return Err(runtime_err("clamp() max must be a number")),
10135                };
10136                let result = self
10137                    .engine()
10138                    .clamp(df, &column, min_val, max_val)
10139                    .map_err(runtime_err)?;
10140                Ok(VmValue::Table(VmTable { df: result }))
10141            }
10142            "data_profile" => {
10143                let result = self.engine().data_profile(df).map_err(runtime_err)?;
10144                Ok(VmValue::Table(VmTable { df: result }))
10145            }
10146            "row_count" => {
10147                let count = self.engine().row_count(df).map_err(runtime_err)?;
10148                Ok(VmValue::Int(count))
10149            }
10150            "null_rate" => {
10151                if ast_args.is_empty() {
10152                    return Err(runtime_err("null_rate() expects (column)"));
10153                }
10154                let column = self.eval_ast_to_string(&ast_args[0])?;
10155                let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10156                Ok(VmValue::Float(rate))
10157            }
10158            "is_unique" => {
10159                if ast_args.is_empty() {
10160                    return Err(runtime_err("is_unique() expects (column)"));
10161                }
10162                let column = self.eval_ast_to_string(&ast_args[0])?;
10163                let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10164                Ok(VmValue::Bool(unique))
10165            }
10166            // Phase F2: Window functions
10167            "window" => {
10168                use tl_data::datafusion::logical_expr::{
10169                    WindowFrame, WindowFunctionDefinition,
10170                    expr::{Sort as DfSort, WindowFunction as WinFunc},
10171                };
10172                if ast_args.is_empty() {
10173                    return Err(runtime_err(
10174                        "window() expects named arguments: fn, partition_by, order_by, alias",
10175                    ));
10176                }
10177                let mut win_fn_name = String::new();
10178                let mut partition_by_cols: Vec<String> = Vec::new();
10179                let mut order_by_cols: Vec<String> = Vec::new();
10180                let mut alias_name = String::new();
10181                let mut win_args: Vec<String> = Vec::new();
10182                let mut descending = false;
10183
10184                for arg in &ast_args {
10185                    if let AstExpr::NamedArg { name, value } = arg {
10186                        match name.as_str() {
10187                            "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10188                            "partition_by" => match value.as_ref() {
10189                                AstExpr::List(items) => {
10190                                    for item in items {
10191                                        partition_by_cols.push(self.eval_ast_to_string(item)?);
10192                                    }
10193                                }
10194                                _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10195                            },
10196                            "order_by" => match value.as_ref() {
10197                                AstExpr::List(items) => {
10198                                    for item in items {
10199                                        order_by_cols.push(self.eval_ast_to_string(item)?);
10200                                    }
10201                                }
10202                                _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10203                            },
10204                            "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10205                            "args" => match value.as_ref() {
10206                                AstExpr::List(items) => {
10207                                    for item in items {
10208                                        win_args.push(self.eval_ast_to_string(item)?);
10209                                    }
10210                                }
10211                                _ => win_args.push(self.eval_ast_to_string(value)?),
10212                            },
10213                            "desc" => {
10214                                if let AstExpr::Bool(b) = value.as_ref() {
10215                                    descending = *b;
10216                                }
10217                            }
10218                            _ => {}
10219                        }
10220                    }
10221                }
10222
10223                if win_fn_name.is_empty() {
10224                    return Err(runtime_err(
10225                        "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10226                    ));
10227                }
10228                if alias_name.is_empty() {
10229                    alias_name = win_fn_name.clone();
10230                }
10231
10232                // Build window function definition
10233                let session = self.engine().session_ctx();
10234                let win_udf = match win_fn_name.as_str() {
10235                    "rank" => session.udwf("rank"),
10236                    "dense_rank" => session.udwf("dense_rank"),
10237                    "row_number" => session.udwf("row_number"),
10238                    "percent_rank" => session.udwf("percent_rank"),
10239                    "cume_dist" => session.udwf("cume_dist"),
10240                    "ntile" => session.udwf("ntile"),
10241                    "lag" => session.udwf("lag"),
10242                    "lead" => session.udwf("lead"),
10243                    "first_value" => session.udwf("first_value"),
10244                    "last_value" => session.udwf("last_value"),
10245                    _ => {
10246                        return Err(runtime_err(format!(
10247                            "Unknown window function: {win_fn_name}"
10248                        )));
10249                    }
10250                }
10251                .map_err(|e| {
10252                    runtime_err(format!(
10253                        "Window function '{win_fn_name}' not available: {e}"
10254                    ))
10255                })?;
10256
10257                let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10258
10259                // Build function args (for lag/lead/ntile)
10260                let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10261                    .iter()
10262                    .map(|a| {
10263                        if let Ok(n) = a.parse::<i64>() {
10264                            lit(n)
10265                        } else {
10266                            col(a.as_str())
10267                        }
10268                    })
10269                    .collect();
10270
10271                let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10272                    partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10273                let order_exprs: Vec<DfSort> = order_by_cols
10274                    .iter()
10275                    .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10276                    .collect();
10277
10278                let has_order = !order_exprs.is_empty();
10279                let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10280                    fun,
10281                    args: func_args,
10282                    partition_by: partition_exprs,
10283                    order_by: order_exprs,
10284                    window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10285                    null_treatment: None,
10286                })
10287                .alias(&alias_name);
10288
10289                // Get all existing columns and add the window column
10290                let schema = df.schema();
10291                let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10292                    .fields()
10293                    .iter()
10294                    .map(|f| col(f.name().as_str()))
10295                    .collect();
10296                select_exprs.push(win_expr);
10297
10298                let result_df = df
10299                    .select(select_exprs)
10300                    .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10301                Ok(VmValue::Table(VmTable { df: result_df }))
10302            }
10303            // Phase F3: Union
10304            "union" => {
10305                if ast_args.is_empty() {
10306                    return Err(runtime_err("union() expects a table argument"));
10307                }
10308                let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10309                let right_df = match right_table {
10310                    VmValue::Table(t) => t.df,
10311                    _ => return Err(runtime_err("union() argument must be a table")),
10312                };
10313                let result_df = df
10314                    .union(right_df)
10315                    .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10316                Ok(VmValue::Table(VmTable { df: result_df }))
10317            }
10318            // Phase F4: Table sampling
10319            "sample" => {
10320                use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10321                use tl_data::datafusion::datasource::MemTable;
10322                if ast_args.is_empty() {
10323                    return Err(runtime_err("sample() expects a count or fraction"));
10324                }
10325                let batches = self.engine().collect(df).map_err(runtime_err)?;
10326                let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10327                let sample_count = match &ast_args[0] {
10328                    AstExpr::Int(n) => (*n as usize).min(total_rows),
10329                    AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10330                        ((total_rows as f64) * f).ceil() as usize
10331                    }
10332                    _ => {
10333                        let val = self.eval_ast_to_string(&ast_args[0])?;
10334                        val.parse::<usize>().map_err(|_| {
10335                            runtime_err("sample() expects integer count or float fraction")
10336                        })?
10337                    }
10338                };
10339                if total_rows == 0 || sample_count == 0 {
10340                    let schema = batches[0].schema();
10341                    let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10342                        schema.clone(),
10343                    );
10344                    let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10345                        .map_err(|e| runtime_err(format!("{e}")))?;
10346                    let new_df = self
10347                        .engine()
10348                        .session_ctx()
10349                        .read_table(Arc::new(mem_table))
10350                        .map_err(|e| runtime_err(format!("{e}")))?;
10351                    return Ok(VmValue::Table(VmTable { df: new_df }));
10352                }
10353                // Random sampling
10354                let mut rng = rand::thread_rng();
10355                let mut indices: Vec<usize> = (0..total_rows).collect();
10356                use rand::seq::SliceRandom;
10357                indices.partial_shuffle(&mut rng, sample_count);
10358                indices.truncate(sample_count);
10359                indices.sort();
10360                // Concatenate and take
10361                let combined = compute::concat_batches(&batches[0].schema(), &batches)
10362                    .map_err(|e| runtime_err(format!("{e}")))?;
10363                let idx_array =
10364                    UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10365                let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10366                    .num_columns())
10367                    .map(|c| {
10368                        compute::take(combined.column(c), &idx_array, None)
10369                            .map_err(|e| runtime_err(format!("{e}")))
10370                    })
10371                    .collect::<Result<Vec<_>, _>>()?;
10372                let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10373                    combined.schema(),
10374                    sampled_cols,
10375                )
10376                .map_err(|e| runtime_err(format!("{e}")))?;
10377                let mem_table =
10378                    MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10379                        .map_err(|e| runtime_err(format!("{e}")))?;
10380                let new_df = self
10381                    .engine()
10382                    .session_ctx()
10383                    .read_table(Arc::new(mem_table))
10384                    .map_err(|e| runtime_err(format!("{e}")))?;
10385                Ok(VmValue::Table(VmTable { df: new_df }))
10386            }
10387            _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10388        }
10389    }
10390
10391    /// Fallback for table pipe when left side is not a table.
10392    /// Converts to a regular function/builtin call with left as first arg.
10393    fn table_pipe_fallback(
10394        &mut self,
10395        left_val: VmValue,
10396        frame_idx: usize,
10397        op_const: u8,
10398        args_const: u8,
10399    ) -> Result<VmValue, TlError> {
10400        let frame = &self.frames[frame_idx];
10401        let op_name = match &frame.prototype.constants[op_const as usize] {
10402            Constant::String(s) => s.to_string(),
10403            _ => return Err(runtime_err("Expected string constant for table op")),
10404        };
10405        let ast_args = match &frame.prototype.constants[args_const as usize] {
10406            Constant::AstExprList(args) => args.clone(),
10407            _ => return Err(runtime_err("Expected AST expr list for table args")),
10408        };
10409
10410        // Try as builtin with left as first arg
10411        if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10412            // Evaluate AST args to VM values
10413            let mut all_args = vec![left_val];
10414            for arg in &ast_args {
10415                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10416            }
10417            let args_base = self.stack.len();
10418            for arg in &all_args {
10419                self.stack.push(arg.clone());
10420            }
10421            let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10422            self.stack.truncate(args_base);
10423            return result;
10424        }
10425
10426        // Try as user-defined function
10427        if let Some(func) = self.globals.get(&op_name).cloned() {
10428            let mut all_args = vec![left_val];
10429            for arg in &ast_args {
10430                all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10431            }
10432            return self.call_vm_function(&func, &all_args);
10433        }
10434
10435        Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10436    }
10437
10438    /// Build TranslateContext from VM globals and stack.
10439    #[cfg(feature = "native")]
10440    fn build_translate_context(&self) -> TranslateContext {
10441        let mut ctx = TranslateContext::new();
10442        // Add globals
10443        for (name, val) in &self.globals {
10444            let local = match val {
10445                VmValue::Int(n) => Some(LocalValue::Int(*n)),
10446                VmValue::Float(f) => Some(LocalValue::Float(*f)),
10447                VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10448                VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10449                _ => None,
10450            };
10451            if let Some(l) = local {
10452                ctx.locals.insert(name.clone(), l);
10453            }
10454        }
10455        // Add locals from current frame
10456        if let Some(frame) = self.frames.last() {
10457            for local_idx in 0..frame.prototype.num_locals as usize {
10458                if let Some(val) = self.stack.get(frame.base + local_idx) {
10459                    // We'd need local name info — for now, rely on globals
10460                    let _ = val;
10461                }
10462            }
10463        }
10464        ctx
10465    }
10466
10467    /// Evaluate an AST expression to a VmValue.
10468    /// For simple expressions does direct lookup; for complex ones, compiles and runs.
10469    fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
10470        match expr {
10471            AstExpr::Ident(name) => {
10472                // Look up in globals first
10473                if let Some(val) = self.globals.get(name) {
10474                    return Ok(val.clone());
10475                }
10476                // Check current frame's stack
10477                if let Some(frame) = self.frames.last() {
10478                    for i in 0..frame.prototype.num_registers as usize {
10479                        if let Some(val) = self.stack.get(frame.base + i)
10480                            && !matches!(val, VmValue::None)
10481                        {
10482                            // Without name->register mapping, we can't be sure
10483                            // which register holds this variable
10484                        }
10485                    }
10486                }
10487                Err(runtime_err(format!("Undefined variable: `{name}`")))
10488            }
10489            AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
10490            AstExpr::Int(n) => Ok(VmValue::Int(*n)),
10491            AstExpr::Float(f) => Ok(VmValue::Float(*f)),
10492            AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
10493            AstExpr::None => Ok(VmValue::None),
10494            AstExpr::Closure {
10495                params: _, body: _, ..
10496            } => {
10497                use crate::compiler;
10498                let wrapper = tl_ast::Program {
10499                    statements: vec![tl_ast::Stmt {
10500                        kind: tl_ast::StmtKind::Expr(expr.clone()),
10501                        span: tl_errors::Span::new(0, 0),
10502                        doc_comment: None,
10503                    }],
10504                    module_doc: None,
10505                };
10506                let proto = compiler::compile(&wrapper)?;
10507                let mut temp_vm = Vm::new();
10508                // Copy globals
10509                temp_vm.globals = self.globals.clone();
10510                let result = temp_vm.execute(&proto)?;
10511                Ok(result)
10512            }
10513            _ => {
10514                // For complex expressions, compile and evaluate
10515                let wrapper = tl_ast::Program {
10516                    statements: vec![tl_ast::Stmt {
10517                        kind: tl_ast::StmtKind::Expr(expr.clone()),
10518                        span: tl_errors::Span::new(0, 0),
10519                        doc_comment: None,
10520                    }],
10521                    module_doc: None,
10522                };
10523                use crate::compiler;
10524                let proto = compiler::compile(&wrapper)?;
10525                let mut temp_vm = Vm::new();
10526                temp_vm.globals = self.globals.clone();
10527                temp_vm.execute(&proto)
10528            }
10529        }
10530    }
10531
10532    fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
10533        match self.eval_ast_to_vm(expr)? {
10534            VmValue::String(s) => Ok(s.to_string()),
10535            _ => Err(runtime_err("Expected a string")),
10536        }
10537    }
10538
10539    /// Simple string interpolation.
10540    fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
10541        let mut result = String::new();
10542        let mut chars = s.chars().peekable();
10543        while let Some(ch) = chars.next() {
10544            if ch == '{' {
10545                let mut var_name = String::new();
10546                let mut depth = 1;
10547                for c in chars.by_ref() {
10548                    if c == '{' {
10549                        depth += 1;
10550                    } else if c == '}' {
10551                        depth -= 1;
10552                        if depth == 0 {
10553                            break;
10554                        }
10555                    }
10556                    var_name.push(c);
10557                }
10558                // Look up variable
10559                if let Some(val) = self.globals.get(&var_name) {
10560                    result.push_str(&format!("{val}"));
10561                } else {
10562                    // Check locals in current frame
10563                    // For now, fall back to globals only — local name info
10564                    // would need debug symbols from the compiler
10565                    result.push('{');
10566                    result.push_str(&var_name);
10567                    result.push('}');
10568                }
10569            } else if ch == '\\' {
10570                match chars.next() {
10571                    Some('n') => result.push('\n'),
10572                    Some('t') => result.push('\t'),
10573                    Some('\\') => result.push('\\'),
10574                    Some('"') => result.push('"'),
10575                    Some(c) => {
10576                        result.push('\\');
10577                        result.push(c);
10578                    }
10579                    None => result.push('\\'),
10580                }
10581            } else {
10582                result.push(ch);
10583            }
10584        }
10585        Ok(result)
10586    }
10587
10588    /// Execute a single bytecode instruction at the given base offset.
10589    /// Used by the LLVM backend's Tier 3 fallback to run complex opcodes on the VM.
10590    pub fn execute_single_instruction(
10591        &mut self,
10592        inst: u32,
10593        proto: &Prototype,
10594        base: usize,
10595    ) -> Result<Option<VmValue>, TlError> {
10596        use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
10597
10598        let proto = Arc::new(proto.clone());
10599        // Push a temporary call frame so the VM can resolve constants etc.
10600        self.frames.push(CallFrame {
10601            prototype: proto.clone(),
10602            ip: 0,
10603            base,
10604            upvalues: Vec::new(),
10605        });
10606        let frame_idx = self.frames.len() - 1;
10607
10608        let op = decode_op(inst);
10609        let a = decode_a(inst);
10610        let _b = decode_b(inst);
10611        let _c = decode_c(inst);
10612        let bx = decode_bx(inst);
10613
10614        // Dispatch the single opcode. We handle the most common
10615        // Tier 3 ops here; anything not handled returns Ok(None).
10616        let result = match op {
10617            Op::GetGlobal => {
10618                let name = self.get_string_constant(frame_idx, bx)?;
10619                let val = self
10620                    .globals
10621                    .get(name.as_ref())
10622                    .cloned()
10623                    .unwrap_or(VmValue::None);
10624                self.stack[base + a as usize] = val;
10625                Ok(None)
10626            }
10627            Op::SetGlobal => {
10628                let name = self.get_string_constant(frame_idx, bx)?;
10629                let val = self.stack[base + a as usize].clone();
10630                self.globals.insert(name.to_string(), val);
10631                Ok(None)
10632            }
10633            _ => {
10634                // For opcodes not explicitly handled, return Ok — the caller
10635                // should have handled Tier 1/2 in LLVM IR.
10636                Ok(None)
10637            }
10638        };
10639
10640        self.frames.pop();
10641        result
10642    }
10643}
10644
10645impl Default for Vm {
10646    fn default() -> Self {
10647        Self::new()
10648    }
10649}
10650
10651#[cfg(test)]
10652mod tests {
10653    use super::*;
10654    use crate::compiler::compile;
10655    use tl_parser::parse;
10656
10657    fn run(source: &str) -> Result<VmValue, TlError> {
10658        let program = parse(source)?;
10659        let proto = compile(&program)?;
10660        let mut vm = Vm::new();
10661        vm.execute(&proto)
10662    }
10663
10664    fn run_output(source: &str) -> Vec<String> {
10665        let program = parse(source).unwrap();
10666        let proto = compile(&program).unwrap();
10667        let mut vm = Vm::new();
10668        vm.execute(&proto).unwrap();
10669        vm.output
10670    }
10671
10672    #[test]
10673    fn test_vm_arithmetic() {
10674        assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
10675        assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
10676        assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
10677        assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
10678        assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
10679        assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
10680        let output = run_output("print(1 + 2)");
10681        assert_eq!(output, vec!["3"]);
10682    }
10683
10684    #[test]
10685    fn test_vm_let_and_print() {
10686        let output = run_output("let x = 42\nprint(x)");
10687        assert_eq!(output, vec!["42"]);
10688    }
10689
10690    #[test]
10691    fn test_vm_function() {
10692        let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
10693        assert_eq!(output, vec!["42"]);
10694    }
10695
10696    #[test]
10697    fn test_vm_if_else() {
10698        let output =
10699            run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
10700        assert_eq!(output, vec!["big"]);
10701    }
10702
10703    #[test]
10704    fn test_vm_list() {
10705        let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
10706        assert_eq!(output, vec!["3"]);
10707    }
10708
10709    #[test]
10710    fn test_vm_map_builtin() {
10711        let output = run_output(
10712            "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
10713        );
10714        assert_eq!(output, vec!["[2, 4, 6]"]);
10715    }
10716
10717    #[test]
10718    fn test_vm_filter_builtin() {
10719        let output = run_output(
10720            "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
10721        );
10722        assert_eq!(output, vec!["[2, 4]"]);
10723    }
10724
10725    #[test]
10726    fn test_vm_for_loop() {
10727        let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
10728        assert_eq!(output, vec!["10"]);
10729    }
10730
10731    #[test]
10732    fn test_vm_closure() {
10733        let output = run_output("let double = (x) => x * 2\nprint(double(5))");
10734        assert_eq!(output, vec!["10"]);
10735    }
10736
10737    #[test]
10738    fn test_vm_sum() {
10739        let output = run_output("print(sum([1, 2, 3, 4]))");
10740        assert_eq!(output, vec!["10"]);
10741    }
10742
10743    #[test]
10744    fn test_vm_reduce() {
10745        let output = run_output(
10746            "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
10747        );
10748        assert_eq!(output, vec!["24"]);
10749    }
10750
10751    #[test]
10752    fn test_vm_pipe() {
10753        let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
10754        assert_eq!(output, vec!["[11, 12, 13]"]);
10755    }
10756
10757    #[test]
10758    fn test_vm_comparison() {
10759        let output = run_output("print(5 > 3)");
10760        assert_eq!(output, vec!["true"]);
10761    }
10762
10763    #[test]
10764    fn test_vm_precedence() {
10765        let output = run_output("print(2 + 3 * 4)");
10766        assert_eq!(output, vec!["14"]);
10767    }
10768
10769    #[test]
10770    fn test_vm_match() {
10771        let output =
10772            run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
10773        assert_eq!(output, vec!["two"]);
10774    }
10775
10776    #[test]
10777    fn test_vm_match_wildcard() {
10778        let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
10779        assert_eq!(output, vec!["other"]);
10780    }
10781
10782    #[test]
10783    fn test_vm_match_binding() {
10784        let output = run_output("print(match 42 { val => val + 1 })");
10785        assert_eq!(output, vec!["43"]);
10786    }
10787
10788    #[test]
10789    fn test_vm_match_guard() {
10790        let output = run_output(
10791            "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10792        );
10793        assert_eq!(output, vec!["pos"]);
10794    }
10795
10796    #[test]
10797    fn test_vm_match_guard_negative() {
10798        let output = run_output(
10799            "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10800        );
10801        assert_eq!(output, vec!["neg"]);
10802    }
10803
10804    #[test]
10805    fn test_vm_match_guard_zero() {
10806        let output = run_output(
10807            "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
10808        );
10809        assert_eq!(output, vec!["zero"]);
10810    }
10811
10812    #[test]
10813    fn test_vm_match_enum_destructure() {
10814        let output = run_output(
10815            r#"
10816enum Shape { Circle(int64), Rect(int64, int64) }
10817let s = Shape::Circle(5)
10818print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10819"#,
10820        );
10821        assert_eq!(output, vec!["5"]);
10822    }
10823
10824    #[test]
10825    fn test_vm_match_enum_destructure_rect() {
10826        let output = run_output(
10827            r#"
10828enum Shape { Circle(int64), Rect(int64, int64) }
10829let s = Shape::Rect(3, 4)
10830print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
10831"#,
10832        );
10833        assert_eq!(output, vec!["12"]);
10834    }
10835
10836    #[test]
10837    fn test_vm_match_enum_wildcard_field() {
10838        let output = run_output(
10839            r#"
10840enum Pair { Two(int64, int64) }
10841let p = Pair::Two(10, 20)
10842print(match p { Pair::Two(_, y) => y, _ => 0 })
10843"#,
10844        );
10845        assert_eq!(output, vec!["20"]);
10846    }
10847
10848    #[test]
10849    fn test_vm_match_enum_guard() {
10850        let output = run_output(
10851            r#"
10852enum Result { Ok(int64), Err(string) }
10853let r = Result::Ok(150)
10854print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
10855"#,
10856        );
10857        assert_eq!(output, vec!["big"]);
10858    }
10859
10860    #[test]
10861    fn test_vm_match_or_pattern() {
10862        let output =
10863            run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10864        assert_eq!(output, vec!["small"]);
10865    }
10866
10867    #[test]
10868    fn test_vm_match_or_pattern_no_match() {
10869        let output =
10870            run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
10871        assert_eq!(output, vec!["big"]);
10872    }
10873
10874    #[test]
10875    fn test_vm_match_string() {
10876        let output = run_output(
10877            r#"let s = "hello"
10878print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
10879        );
10880        assert_eq!(output, vec!["2"]);
10881    }
10882
10883    #[test]
10884    fn test_vm_match_bool() {
10885        let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
10886        assert_eq!(output, vec!["yes"]);
10887    }
10888
10889    #[test]
10890    fn test_vm_match_none() {
10891        let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
10892        assert_eq!(output, vec!["nothing"]);
10893    }
10894
10895    #[test]
10896    fn test_vm_let_destructure_list() {
10897        let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
10898        assert_eq!(output, vec!["1", "2", "3"]);
10899    }
10900
10901    #[test]
10902    fn test_vm_let_destructure_list_rest() {
10903        let output =
10904            run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
10905        assert_eq!(output, vec!["1", "3"]);
10906    }
10907
10908    #[test]
10909    fn test_vm_let_destructure_struct() {
10910        let output = run_output(
10911            r#"
10912struct Point { x: int64, y: int64 }
10913let p = Point { x: 10, y: 20 }
10914let Point { x, y } = p
10915print(x)
10916print(y)
10917"#,
10918        );
10919        assert_eq!(output, vec!["10", "20"]);
10920    }
10921
10922    #[test]
10923    fn test_vm_let_destructure_struct_anon() {
10924        let output = run_output(
10925            r#"
10926struct Point { x: int64, y: int64 }
10927let p = Point { x: 10, y: 20 }
10928let { x, y } = p
10929print(x)
10930print(y)
10931"#,
10932        );
10933        assert_eq!(output, vec!["10", "20"]);
10934    }
10935
10936    #[test]
10937    fn test_vm_match_struct_pattern() {
10938        let output = run_output(
10939            r#"
10940struct Point { x: int64, y: int64 }
10941let p = Point { x: 1, y: 2 }
10942print(match p { Point { x, y } => x + y, _ => 0 })
10943"#,
10944        );
10945        assert_eq!(output, vec!["3"]);
10946    }
10947
10948    #[test]
10949    fn test_vm_match_list_pattern() {
10950        let output = run_output(
10951            r#"
10952let lst = [1, 2, 3]
10953print(match lst { [a, b, c] => a + b + c, _ => 0 })
10954"#,
10955        );
10956        assert_eq!(output, vec!["6"]);
10957    }
10958
10959    #[test]
10960    fn test_vm_match_list_rest_pattern() {
10961        let output = run_output(
10962            r#"
10963let lst = [10, 20, 30, 40]
10964print(match lst { [head, ...rest] => head, _ => 0 })
10965"#,
10966        );
10967        assert_eq!(output, vec!["10"]);
10968    }
10969
10970    #[test]
10971    fn test_vm_match_list_empty() {
10972        let output = run_output(
10973            r#"
10974let lst = []
10975print(match lst { [] => "empty", _ => "nonempty" })
10976"#,
10977        );
10978        assert_eq!(output, vec!["empty"]);
10979    }
10980
10981    #[test]
10982    fn test_vm_match_list_length_mismatch() {
10983        let output = run_output(
10984            r#"
10985let lst = [1, 2, 3]
10986print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
10987"#,
10988        );
10989        assert_eq!(output, vec!["three"]);
10990    }
10991
10992    #[test]
10993    fn test_vm_match_negative_literal() {
10994        let output =
10995            run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
10996        assert_eq!(output, vec!["neg one"]);
10997    }
10998
10999    #[test]
11000    fn test_vm_case_with_pattern() {
11001        let output = run_output(
11002            r#"
11003let x = 5
11004let result = case {
11005    x > 10 => "big",
11006    x > 0 => "positive",
11007    _ => "other"
11008}
11009print(result)
11010"#,
11011        );
11012        assert_eq!(output, vec!["positive"]);
11013    }
11014
11015    #[test]
11016    fn test_vm_parallel_map() {
11017        // Build a range > PARALLEL_THRESHOLD and map with a pure function
11018        let result = run("map(range(15000), (x) => x * 2)").unwrap();
11019        if let VmValue::List(items) = result {
11020            assert_eq!(items.len(), 15000);
11021            assert!(matches!(items[0], VmValue::Int(0)));
11022            assert!(matches!(items[1], VmValue::Int(2)));
11023            assert!(matches!(items[14999], VmValue::Int(29998)));
11024        } else {
11025            panic!("Expected list, got {:?}", result);
11026        }
11027    }
11028
11029    #[test]
11030    fn test_vm_parallel_filter() {
11031        let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11032        if let VmValue::List(items) = result {
11033            assert_eq!(items.len(), 10000);
11034            assert!(matches!(items[0], VmValue::Int(0)));
11035            assert!(matches!(items[1], VmValue::Int(2)));
11036        } else {
11037            panic!("Expected list, got {:?}", result);
11038        }
11039    }
11040
11041    #[test]
11042    fn test_vm_parallel_sum() {
11043        let result = run("sum(range(20000))").unwrap();
11044        // sum(0..19999) = 19999 * 20000 / 2 = 199990000
11045        assert!(matches!(result, VmValue::Int(199990000)));
11046    }
11047
11048    #[test]
11049    fn test_vm_recursive_fib() {
11050        let output = run_output(
11051            "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11052        );
11053        assert_eq!(output, vec!["55"]);
11054    }
11055
11056    #[test]
11057    fn test_vm_if_else_expr() {
11058        // if-else as the last expression in a function should return a value
11059        let output = run_output(
11060            "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11061        );
11062        assert_eq!(output, vec!["5", "3"]);
11063    }
11064
11065    // ── Phase 5 tests ──
11066
11067    #[test]
11068    fn test_vm_struct_creation() {
11069        let output = run_output(
11070            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11071        );
11072        assert_eq!(output, vec!["1.0", "2.0"]);
11073    }
11074
11075    #[test]
11076    fn test_vm_struct_nested() {
11077        let output = run_output(
11078            "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)",
11079        );
11080        assert_eq!(output, vec!["0.0"]);
11081    }
11082
11083    #[test]
11084    fn test_vm_enum_creation() {
11085        let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11086        assert_eq!(output, vec!["Color::Red"]);
11087    }
11088
11089    #[test]
11090    fn test_vm_enum_with_fields() {
11091        let output = run_output(
11092            "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11093        );
11094        assert!(output[0].contains("Circle"));
11095    }
11096
11097    #[test]
11098    fn test_vm_impl_method() {
11099        let output = run_output(
11100            "struct Counter { value: int64 }\nimpl Counter {\n    fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11101        );
11102        assert_eq!(output, vec!["42"]);
11103    }
11104
11105    #[test]
11106    fn test_vm_try_catch_throw() {
11107        let output = run_output("try {\n    throw \"oops\"\n} catch e {\n    print(e)\n}");
11108        assert_eq!(output, vec!["oops"]);
11109    }
11110
11111    #[test]
11112    fn test_vm_string_split() {
11113        let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11114        assert_eq!(output, vec!["[hello, world]"]);
11115    }
11116
11117    #[test]
11118    fn test_vm_string_trim() {
11119        let output = run_output("print(\"  hello  \".trim())");
11120        assert_eq!(output, vec!["hello"]);
11121    }
11122
11123    #[test]
11124    fn test_vm_string_contains() {
11125        let output = run_output("print(\"hello world\".contains(\"world\"))");
11126        assert_eq!(output, vec!["true"]);
11127    }
11128
11129    #[test]
11130    fn test_vm_string_upper_lower() {
11131        let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11132        assert_eq!(output, vec!["HELLO", "hello"]);
11133    }
11134
11135    #[test]
11136    fn test_vm_math_sqrt() {
11137        let output = run_output("print(sqrt(16.0))");
11138        assert_eq!(output, vec!["4.0"]);
11139    }
11140
11141    #[test]
11142    fn test_vm_math_floor_ceil() {
11143        let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11144        assert_eq!(output, vec!["3.0", "4.0"]);
11145    }
11146
11147    #[test]
11148    fn test_vm_math_trig() {
11149        let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11150        assert_eq!(output, vec!["0.0", "1.0"]);
11151    }
11152
11153    #[test]
11154    fn test_vm_assert_pass() {
11155        run("assert(true)").unwrap();
11156        run("assert_eq(1 + 1, 2)").unwrap();
11157    }
11158
11159    #[test]
11160    fn test_vm_assert_fail() {
11161        assert!(run("assert(false)").is_err());
11162        assert!(run("assert_eq(1, 2)").is_err());
11163    }
11164
11165    #[test]
11166    fn test_vm_join() {
11167        let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11168        assert_eq!(output, vec!["a, b, c"]);
11169    }
11170
11171    #[test]
11172    fn test_vm_list_method_len() {
11173        let output = run_output("print([1, 2, 3].len())");
11174        assert_eq!(output, vec!["3"]);
11175    }
11176
11177    #[test]
11178    fn test_vm_list_method_map() {
11179        let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11180        assert_eq!(output, vec!["[2, 4, 6]"]);
11181    }
11182
11183    #[test]
11184    fn test_vm_list_method_filter() {
11185        let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11186        assert_eq!(output, vec!["[4, 5]"]);
11187    }
11188
11189    #[test]
11190    fn test_vm_string_replace() {
11191        let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11192        assert_eq!(output, vec!["hello rust"]);
11193    }
11194
11195    #[test]
11196    fn test_vm_string_starts_ends() {
11197        let output = run_output(
11198            "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11199        );
11200        assert_eq!(output, vec!["true", "true"]);
11201    }
11202
11203    #[test]
11204    fn test_vm_math_log() {
11205        let result = run("log(1.0)").unwrap();
11206        if let VmValue::Float(f) = result {
11207            assert!((f - 0.0).abs() < 1e-10);
11208        } else {
11209            panic!("Expected float");
11210        }
11211    }
11212
11213    #[test]
11214    fn test_vm_pow_builtin() {
11215        let output = run_output("print(pow(2.0, 10.0))");
11216        assert_eq!(output, vec!["1024.0"]);
11217    }
11218
11219    #[test]
11220    fn test_vm_round_builtin() {
11221        let output = run_output("print(round(3.5))");
11222        assert_eq!(output, vec!["4.0"]);
11223    }
11224
11225    #[test]
11226    fn test_vm_try_catch_runtime_error() {
11227        let output = run_output("try {\n    let x = 1 / 0\n} catch e {\n    print(e)\n}");
11228        assert_eq!(output, vec!["Division by zero"]);
11229    }
11230
11231    #[test]
11232    fn test_vm_struct_field_access() {
11233        let output = run_output(
11234            "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11235        );
11236        assert_eq!(output, vec!["1.5"]);
11237    }
11238
11239    #[test]
11240    fn test_vm_enum_match() {
11241        let output = run_output(
11242            "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11243        );
11244        // match expression compares enum instances
11245        assert!(!output.is_empty());
11246    }
11247
11248    #[test]
11249    fn test_vm_impl_method_with_args() {
11250        let output = run_output(
11251            "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())",
11252        );
11253        assert_eq!(output, vec!["12.0"]);
11254    }
11255
11256    #[test]
11257    fn test_vm_string_len() {
11258        let output = run_output("print(\"hello\".len())");
11259        assert_eq!(output, vec!["5"]);
11260    }
11261
11262    #[test]
11263    fn test_vm_list_reduce() {
11264        let output = run_output(
11265            "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11266        );
11267        assert_eq!(output, vec!["10"]);
11268    }
11269
11270    #[test]
11271    fn test_vm_nested_try_catch() {
11272        let output = run_output(
11273            "try {\n    try {\n        throw \"inner\"\n    } catch e {\n        print(e)\n        throw \"outer\"\n    }\n} catch e2 {\n    print(e2)\n}",
11274        );
11275        assert_eq!(output, vec!["inner", "outer"]);
11276    }
11277
11278    #[test]
11279    fn test_vm_math_pow() {
11280        let output = run_output("print(pow(2.0, 10.0))");
11281        assert_eq!(output, vec!["1024.0"]);
11282    }
11283
11284    // ── Phase 6: Stdlib & Ecosystem tests ──
11285
11286    #[test]
11287    fn test_vm_json_parse() {
11288        let output = run_output(
11289            r#"let m = map_from("a", 1, "b", "hello")
11290let s = json_stringify(m)
11291let m2 = json_parse(s)
11292print(m2["a"])
11293print(m2["b"])"#,
11294        );
11295        assert_eq!(output, vec!["1", "hello"]);
11296    }
11297
11298    #[test]
11299    fn test_vm_json_stringify() {
11300        let output = run_output(
11301            r#"let m = map_from("x", 1, "y", 2)
11302let s = json_stringify(m)
11303print(s)"#,
11304        );
11305        assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11306    }
11307
11308    #[test]
11309    fn test_vm_map_from_and_access() {
11310        let output = run_output(
11311            r#"let m = map_from("a", 10, "b", 20)
11312print(m["a"])
11313print(m.b)"#,
11314        );
11315        assert_eq!(output, vec!["10", "20"]);
11316    }
11317
11318    #[test]
11319    fn test_vm_map_methods() {
11320        let output = run_output(
11321            r#"let m = map_from("a", 1, "b", 2)
11322print(m.keys())
11323print(m.values())
11324print(m.contains_key("a"))
11325print(m.len())"#,
11326        );
11327        assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11328    }
11329
11330    #[test]
11331    fn test_vm_map_set_index() {
11332        let output = run_output(
11333            r#"let m = map_from("a", 1)
11334m["b"] = 2
11335print(m["b"])"#,
11336        );
11337        assert_eq!(output, vec!["2"]);
11338    }
11339
11340    #[test]
11341    fn test_vm_map_iteration() {
11342        let output = run_output(
11343            r#"let m = map_from("x", 10, "y", 20)
11344for kv in m {
11345    print(kv[0])
11346}"#,
11347        );
11348        assert_eq!(output, vec!["x", "y"]);
11349    }
11350
11351    #[test]
11352    fn test_vm_file_read_write() {
11353        let output = run_output(
11354            r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11355print(read_file("/tmp/tl_vm_test.txt"))
11356print(file_exists("/tmp/tl_vm_test.txt"))"#,
11357        );
11358        assert_eq!(output, vec!["vm hello", "true"]);
11359    }
11360
11361    #[test]
11362    fn test_vm_env_get_set() {
11363        let output = run_output(
11364            r#"env_set("TL_VM_TEST", "abc")
11365print(env_get("TL_VM_TEST"))"#,
11366        );
11367        assert_eq!(output, vec!["abc"]);
11368    }
11369
11370    #[test]
11371    fn test_vm_regex_match() {
11372        let output = run_output(
11373            r#"print(regex_match("\\d+", "abc123"))
11374print(regex_match("^\\d+$", "abc"))"#,
11375        );
11376        assert_eq!(output, vec!["true", "false"]);
11377    }
11378
11379    #[test]
11380    fn test_vm_regex_find() {
11381        let output = run_output(
11382            r#"let m = regex_find("\\d+", "abc123def456")
11383print(len(m))
11384print(m[0])"#,
11385        );
11386        assert_eq!(output, vec!["2", "123"]);
11387    }
11388
11389    #[test]
11390    fn test_vm_regex_replace() {
11391        let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11392        assert_eq!(output, vec!["abcX"]);
11393    }
11394
11395    #[test]
11396    fn test_vm_now() {
11397        // now() returns DateTime which displays as formatted string
11398        let output = run_output("let t = now()\nprint(type_of(t))");
11399        assert_eq!(output, vec!["datetime"]);
11400    }
11401
11402    #[test]
11403    fn test_vm_date_format() {
11404        let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11405        assert_eq!(output, vec!["2024-01-01"]);
11406    }
11407
11408    #[test]
11409    fn test_vm_date_parse() {
11410        let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11411        assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11412    }
11413
11414    #[test]
11415    fn test_vm_string_chars() {
11416        let output = run_output(r#"print(len("hello".chars()))"#);
11417        assert_eq!(output, vec!["5"]);
11418    }
11419
11420    #[test]
11421    fn test_vm_string_repeat() {
11422        let output = run_output(r#"print("ab".repeat(3))"#);
11423        assert_eq!(output, vec!["ababab"]);
11424    }
11425
11426    #[test]
11427    fn test_vm_string_index_of() {
11428        let output = run_output(r#"print("hello world".index_of("world"))"#);
11429        assert_eq!(output, vec!["6"]);
11430    }
11431
11432    #[test]
11433    fn test_vm_string_substring() {
11434        let output = run_output(r#"print("hello world".substring(0, 5))"#);
11435        assert_eq!(output, vec!["hello"]);
11436    }
11437
11438    #[test]
11439    fn test_vm_string_pad() {
11440        let output = run_output(
11441            r#"print("42".pad_left(5, "0"))
11442print("hi".pad_right(5, "."))"#,
11443        );
11444        assert_eq!(output, vec!["00042", "hi..."]);
11445    }
11446
11447    #[test]
11448    fn test_vm_list_sort() {
11449        let output = run_output(r#"print([3, 1, 2].sort())"#);
11450        assert_eq!(output, vec!["[1, 2, 3]"]);
11451    }
11452
11453    #[test]
11454    fn test_vm_list_reverse() {
11455        let output = run_output(r#"print([1, 2, 3].reverse())"#);
11456        assert_eq!(output, vec!["[3, 2, 1]"]);
11457    }
11458
11459    #[test]
11460    fn test_vm_list_contains() {
11461        let output = run_output(
11462            r#"print([1, 2, 3].contains(2))
11463print([1, 2, 3].contains(5))"#,
11464        );
11465        assert_eq!(output, vec!["true", "false"]);
11466    }
11467
11468    #[test]
11469    fn test_vm_list_slice() {
11470        let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
11471        assert_eq!(output, vec!["[2, 3, 4]"]);
11472    }
11473
11474    #[test]
11475    fn test_vm_zip() {
11476        let output = run_output(
11477            r#"let p = zip([1, 2], ["a", "b"])
11478print(p[0])"#,
11479        );
11480        assert_eq!(output, vec!["[1, a]"]);
11481    }
11482
11483    #[test]
11484    fn test_vm_enumerate() {
11485        let output = run_output(
11486            r#"let e = enumerate(["a", "b", "c"])
11487print(e[1])"#,
11488        );
11489        assert_eq!(output, vec!["[1, b]"]);
11490    }
11491
11492    #[test]
11493    fn test_vm_bool() {
11494        let output = run_output(
11495            r#"print(bool(1))
11496print(bool(0))
11497print(bool(""))"#,
11498        );
11499        assert_eq!(output, vec!["true", "false", "false"]);
11500    }
11501
11502    #[test]
11503    fn test_vm_range_step() {
11504        let output = run_output(r#"print(range(0, 10, 3))"#);
11505        assert_eq!(output, vec!["[0, 3, 6, 9]"]);
11506    }
11507
11508    #[test]
11509    fn test_vm_int_bool() {
11510        let output = run_output(
11511            r#"print(int(true))
11512print(int(false))"#,
11513        );
11514        assert_eq!(output, vec!["1", "0"]);
11515    }
11516
11517    #[test]
11518    fn test_vm_map_len_typeof() {
11519        let output = run_output(
11520            r#"let m = map_from("a", 1)
11521print(len(m))
11522print(type_of(m))"#,
11523        );
11524        assert_eq!(output, vec!["1", "map"]);
11525    }
11526
11527    #[test]
11528    fn test_vm_json_file_roundtrip() {
11529        let output = run_output(
11530            r#"let data = map_from("name", "vm_test", "count", 99)
11531write_file("/tmp/tl_vm_json.json", json_stringify(data))
11532let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
11533print(parsed["name"])
11534print(parsed["count"])"#,
11535        );
11536        assert_eq!(output, vec!["vm_test", "99"]);
11537    }
11538
11539    // ── Phase 7: Concurrency tests ──
11540
11541    #[test]
11542    fn test_vm_spawn_await_basic() {
11543        let output = run_output(
11544            r#"fn worker() { 42 }
11545let t = spawn(worker)
11546let result = await t
11547print(result)"#,
11548        );
11549        assert_eq!(output, vec!["42"]);
11550    }
11551
11552    #[test]
11553    fn test_vm_spawn_closure_with_capture() {
11554        let output = run_output(
11555            r#"let x = 10
11556let f = () => x + 5
11557let t = spawn(f)
11558print(await t)"#,
11559        );
11560        assert_eq!(output, vec!["15"]);
11561    }
11562
11563    #[test]
11564    fn test_vm_sleep() {
11565        let output = run_output(
11566            r#"sleep(10)
11567print("done")"#,
11568        );
11569        assert_eq!(output, vec!["done"]);
11570    }
11571
11572    #[test]
11573    fn test_vm_await_non_task_passthrough() {
11574        let output = run_output(r#"print(await 42)"#);
11575        assert_eq!(output, vec!["42"]);
11576    }
11577
11578    #[test]
11579    fn test_vm_spawn_multiple_await() {
11580        let output = run_output(
11581            r#"fn w1() { 1 }
11582fn w2() { 2 }
11583fn w3() { 3 }
11584let t1 = spawn(w1)
11585let t2 = spawn(w2)
11586let t3 = spawn(w3)
11587let a = await t1
11588let b = await t2
11589let c = await t3
11590print(a + b + c)"#,
11591        );
11592        assert_eq!(output, vec!["6"]);
11593    }
11594
11595    #[test]
11596    fn test_vm_channel_basic() {
11597        let output = run_output(
11598            r#"let ch = channel()
11599send(ch, 42)
11600let val = recv(ch)
11601print(val)"#,
11602        );
11603        assert_eq!(output, vec!["42"]);
11604    }
11605
11606    #[test]
11607    fn test_vm_channel_between_tasks() {
11608        let output = run_output(
11609            r#"let ch = channel()
11610fn producer() { send(ch, 100) }
11611let t = spawn(producer)
11612let val = recv(ch)
11613await t
11614print(val)"#,
11615        );
11616        assert_eq!(output, vec!["100"]);
11617    }
11618
11619    #[test]
11620    fn test_vm_try_recv_empty() {
11621        let output = run_output(
11622            r#"let ch = channel()
11623let val = try_recv(ch)
11624print(val)"#,
11625        );
11626        assert_eq!(output, vec!["none"]);
11627    }
11628
11629    #[test]
11630    fn test_vm_channel_multiple_values() {
11631        let output = run_output(
11632            r#"let ch = channel()
11633send(ch, 1)
11634send(ch, 2)
11635send(ch, 3)
11636print(recv(ch))
11637print(recv(ch))
11638print(recv(ch))"#,
11639        );
11640        assert_eq!(output, vec!["1", "2", "3"]);
11641    }
11642
11643    #[test]
11644    fn test_vm_channel_producer_consumer() {
11645        let output = run_output(
11646            r#"let ch = channel()
11647fn producer() {
11648    send(ch, 10)
11649    send(ch, 20)
11650    send(ch, 30)
11651}
11652let t = spawn(producer)
11653let a = recv(ch)
11654let b = recv(ch)
11655let c = recv(ch)
11656await t
11657print(a + b + c)"#,
11658        );
11659        assert_eq!(output, vec!["60"]);
11660    }
11661
11662    #[test]
11663    fn test_vm_await_all() {
11664        let output = run_output(
11665            r#"fn w1() { 10 }
11666fn w2() { 20 }
11667fn w3() { 30 }
11668let t1 = spawn(w1)
11669let t2 = spawn(w2)
11670let t3 = spawn(w3)
11671let results = await_all([t1, t2, t3])
11672print(sum(results))"#,
11673        );
11674        assert_eq!(output, vec!["60"]);
11675    }
11676
11677    #[test]
11678    fn test_vm_pmap_basic() {
11679        let output = run_output(
11680            r#"let results = pmap([1, 2, 3], (x) => x * 2)
11681print(results)"#,
11682        );
11683        assert_eq!(output, vec!["[2, 4, 6]"]);
11684    }
11685
11686    #[test]
11687    fn test_vm_pmap_order_preserved() {
11688        let output = run_output(
11689            r#"let results = pmap([10, 20, 30], (x) => x + 1)
11690print(results)"#,
11691        );
11692        assert_eq!(output, vec!["[11, 21, 31]"]);
11693    }
11694
11695    #[test]
11696    fn test_vm_timeout_success() {
11697        let output = run_output(
11698            r#"fn worker() { 42 }
11699let t = spawn(worker)
11700let result = timeout(t, 5000)
11701print(result)"#,
11702        );
11703        assert_eq!(output, vec!["42"]);
11704    }
11705
11706    #[test]
11707    fn test_vm_timeout_failure() {
11708        let output = run_output(
11709            r#"fn slow() { sleep(10000) }
11710let t = spawn(slow)
11711let result = "ok"
11712try {
11713    result = timeout(t, 50)
11714} catch e {
11715    result = e
11716}
11717print(result)"#,
11718        );
11719        assert_eq!(output, vec!["Task timed out"]);
11720    }
11721
11722    #[test]
11723    fn test_vm_spawn_error_propagation() {
11724        let output = run_output(
11725            r#"fn bad() { throw "bad thing" }
11726let result = "ok"
11727try {
11728    let t = spawn(bad)
11729    result = await t
11730} catch e {
11731    result = e
11732}
11733print(result)"#,
11734        );
11735        assert_eq!(output, vec!["bad thing"]);
11736    }
11737
11738    #[test]
11739    fn test_vm_spawn_producer_consumer_pipeline() {
11740        let output = run_output(
11741            r#"let ch = channel()
11742fn producer() {
11743    let mut i = 0
11744    while i < 5 {
11745        send(ch, i * 10)
11746        i = i + 1
11747    }
11748}
11749let t = spawn(producer)
11750let mut total = 0
11751let mut count = 0
11752while count < 5 {
11753    total = total + recv(ch)
11754    count = count + 1
11755}
11756await t
11757print(total)"#,
11758        );
11759        assert_eq!(output, vec!["100"]);
11760    }
11761
11762    #[test]
11763    fn test_vm_type_of_task_channel() {
11764        let output = run_output(
11765            r#"fn worker() { 1 }
11766let t = spawn(worker)
11767let ch = channel()
11768print(type_of(t))
11769print(type_of(ch))
11770await t"#,
11771        );
11772        assert_eq!(output, vec!["task", "channel"]);
11773    }
11774
11775    // ── Phase 8: Iterators & Generators ──
11776
11777    #[test]
11778    fn test_vm_basic_generator() {
11779        let output = run_output(
11780            r#"fn gen() {
11781    yield 1
11782    yield 2
11783    yield 3
11784}
11785let g = gen()
11786print(next(g))
11787print(next(g))
11788print(next(g))
11789print(next(g))"#,
11790        );
11791        assert_eq!(output, vec!["1", "2", "3", "none"]);
11792    }
11793
11794    #[test]
11795    fn test_vm_generator_exhaustion() {
11796        let output = run_output(
11797            r#"fn gen() {
11798    yield 42
11799}
11800let g = gen()
11801print(next(g))
11802print(next(g))
11803print(next(g))"#,
11804        );
11805        assert_eq!(output, vec!["42", "none", "none"]);
11806    }
11807
11808    #[test]
11809    fn test_vm_generator_with_loop() {
11810        let output = run_output(
11811            r#"fn counter() {
11812    let mut i = 0
11813    while i < 3 {
11814        yield i
11815        i = i + 1
11816    }
11817}
11818let g = counter()
11819print(next(g))
11820print(next(g))
11821print(next(g))
11822print(next(g))"#,
11823        );
11824        assert_eq!(output, vec!["0", "1", "2", "none"]);
11825    }
11826
11827    #[test]
11828    fn test_vm_generator_with_args() {
11829        let output = run_output(
11830            r#"fn count_from(start) {
11831    let mut i = start
11832    while i < start + 3 {
11833        yield i
11834        i = i + 1
11835    }
11836}
11837let g = count_from(10)
11838print(next(g))
11839print(next(g))
11840print(next(g))
11841print(next(g))"#,
11842        );
11843        assert_eq!(output, vec!["10", "11", "12", "none"]);
11844    }
11845
11846    #[test]
11847    fn test_vm_generator_yield_none() {
11848        let output = run_output(
11849            r#"fn gen() {
11850    yield
11851    yield 5
11852}
11853let g = gen()
11854print(next(g))
11855print(next(g))
11856print(next(g))"#,
11857        );
11858        assert_eq!(output, vec!["none", "5", "none"]);
11859    }
11860
11861    #[test]
11862    fn test_vm_is_generator() {
11863        let output = run_output(
11864            r#"fn gen() { yield 1 }
11865let g = gen()
11866print(is_generator(g))
11867print(is_generator(42))
11868print(is_generator(none))"#,
11869        );
11870        assert_eq!(output, vec!["true", "false", "false"]);
11871    }
11872
11873    #[test]
11874    fn test_vm_multiple_generators() {
11875        let output = run_output(
11876            r#"fn gen() {
11877    yield 1
11878    yield 2
11879}
11880let g1 = gen()
11881let g2 = gen()
11882print(next(g1))
11883print(next(g2))
11884print(next(g1))
11885print(next(g2))"#,
11886        );
11887        assert_eq!(output, vec!["1", "1", "2", "2"]);
11888    }
11889
11890    #[test]
11891    fn test_vm_for_over_generator() {
11892        let output = run_output(
11893            r#"fn gen() {
11894    yield 10
11895    yield 20
11896    yield 30
11897}
11898for x in gen() {
11899    print(x)
11900}"#,
11901        );
11902        assert_eq!(output, vec!["10", "20", "30"]);
11903    }
11904
11905    #[test]
11906    fn test_vm_iter_builtin() {
11907        let output = run_output(
11908            r#"let g = iter([1, 2, 3])
11909print(next(g))
11910print(next(g))
11911print(next(g))
11912print(next(g))"#,
11913        );
11914        assert_eq!(output, vec!["1", "2", "3", "none"]);
11915    }
11916
11917    #[test]
11918    fn test_vm_take_builtin() {
11919        let output = run_output(
11920            r#"fn naturals() {
11921    let mut n = 0
11922    while true {
11923        yield n
11924        n = n + 1
11925    }
11926}
11927let g = take(naturals(), 5)
11928print(next(g))
11929print(next(g))
11930print(next(g))
11931print(next(g))
11932print(next(g))
11933print(next(g))"#,
11934        );
11935        assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
11936    }
11937
11938    #[test]
11939    fn test_vm_skip_builtin() {
11940        let output = run_output(
11941            r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
11942print(next(g))
11943print(next(g))
11944print(next(g))
11945print(next(g))"#,
11946        );
11947        assert_eq!(output, vec!["30", "40", "50", "none"]);
11948    }
11949
11950    #[test]
11951    fn test_vm_gen_collect() {
11952        let output = run_output(
11953            r#"fn gen() {
11954    yield 1
11955    yield 2
11956    yield 3
11957}
11958let result = gen_collect(gen())
11959print(result)"#,
11960        );
11961        assert_eq!(output, vec!["[1, 2, 3]"]);
11962    }
11963
11964    #[test]
11965    fn test_vm_gen_map() {
11966        let output = run_output(
11967            r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
11968print(gen_collect(g))"#,
11969        );
11970        assert_eq!(output, vec!["[10, 20, 30]"]);
11971    }
11972
11973    #[test]
11974    fn test_vm_gen_filter() {
11975        let output = run_output(
11976            r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
11977print(gen_collect(g))"#,
11978        );
11979        assert_eq!(output, vec!["[2, 4, 6]"]);
11980    }
11981
11982    #[test]
11983    fn test_vm_chain() {
11984        let output = run_output(
11985            r#"let g = chain(iter([1, 2]), iter([3, 4]))
11986print(gen_collect(g))"#,
11987        );
11988        assert_eq!(output, vec!["[1, 2, 3, 4]"]);
11989    }
11990
11991    #[test]
11992    fn test_vm_gen_zip() {
11993        let output = run_output(
11994            r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
11995print(gen_collect(g))"#,
11996        );
11997        assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
11998    }
11999
12000    #[test]
12001    fn test_vm_gen_enumerate() {
12002        let output = run_output(
12003            r#"let g = gen_enumerate(iter([10, 20, 30]))
12004print(gen_collect(g))"#,
12005        );
12006        assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12007    }
12008
12009    #[test]
12010    fn test_vm_combinator_chaining() {
12011        let output = run_output(
12012            r#"fn naturals() {
12013    let mut n = 0
12014    while true {
12015        yield n
12016        n = n + 1
12017    }
12018}
12019let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12020print(result)"#,
12021        );
12022        assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12023    }
12024
12025    #[test]
12026    fn test_vm_for_over_take() {
12027        let output = run_output(
12028            r#"fn naturals() {
12029    let mut n = 0
12030    while true {
12031        yield n
12032        n = n + 1
12033    }
12034}
12035for x in take(naturals(), 5) {
12036    print(x)
12037}"#,
12038        );
12039        assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12040    }
12041
12042    #[test]
12043    fn test_vm_generator_error_propagation() {
12044        let result = run(r#"fn bad_gen() {
12045    yield 1
12046    throw "oops"
12047}
12048let g = bad_gen()
12049let mut caught = ""
12050next(g)
12051try {
12052    next(g)
12053} catch e {
12054    caught = e
12055}
12056print(caught)"#);
12057        // Should succeed (error caught)
12058        assert!(result.is_ok());
12059    }
12060
12061    #[test]
12062    fn test_vm_fibonacci_generator() {
12063        let output = run_output(
12064            r#"fn fib() {
12065    let mut a = 0
12066    let mut b = 1
12067    while true {
12068        yield a
12069        let temp = a + b
12070        a = b
12071        b = temp
12072    }
12073}
12074print(gen_collect(take(fib(), 8)))"#,
12075        );
12076        assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12077    }
12078
12079    #[test]
12080    fn test_vm_generator_method_syntax() {
12081        let output = run_output(
12082            r#"fn gen() {
12083    yield 1
12084    yield 2
12085    yield 3
12086}
12087let g = gen()
12088print(type_of(g))"#,
12089        );
12090        assert_eq!(output, vec!["generator"]);
12091    }
12092
12093    // ── Phase 10: Result/Option + ? operator tests ──
12094
12095    #[test]
12096    fn test_vm_ok_err_builtins() {
12097        let output = run_output("let r = Ok(42)\nprint(r)");
12098        assert_eq!(output, vec!["Result::Ok(42)"]);
12099
12100        let output = run_output("let r = Err(\"fail\")\nprint(r)");
12101        assert_eq!(output, vec!["Result::Err(fail)"]);
12102    }
12103
12104    #[test]
12105    fn test_vm_is_ok_is_err() {
12106        let output = run_output("print(is_ok(Ok(42)))");
12107        assert_eq!(output, vec!["true"]);
12108        let output = run_output("print(is_err(Ok(42)))");
12109        assert_eq!(output, vec!["false"]);
12110        let output = run_output("print(is_ok(Err(\"fail\")))");
12111        assert_eq!(output, vec!["false"]);
12112        let output = run_output("print(is_err(Err(\"fail\")))");
12113        assert_eq!(output, vec!["true"]);
12114    }
12115
12116    #[test]
12117    fn test_vm_unwrap_ok() {
12118        let output = run_output("print(unwrap(Ok(42)))");
12119        assert_eq!(output, vec!["42"]);
12120    }
12121
12122    #[test]
12123    fn test_vm_unwrap_err_panics() {
12124        let result = run("unwrap(Err(\"fail\"))");
12125        assert!(result.is_err());
12126    }
12127
12128    #[test]
12129    fn test_vm_try_on_ok() {
12130        let output = run_output(
12131            r#"fn get_val() { Ok(42) }
12132fn process() { let v = get_val()? + 1
12133Ok(v) }
12134print(process())"#,
12135        );
12136        assert_eq!(output, vec!["Result::Ok(43)"]);
12137    }
12138
12139    #[test]
12140    fn test_vm_try_on_err_propagates() {
12141        let output = run_output(
12142            r#"fn failing() { Err("oops") }
12143fn process() { let v = failing()?
12144Ok(v) }
12145print(process())"#,
12146        );
12147        assert_eq!(output, vec!["Result::Err(oops)"]);
12148    }
12149
12150    #[test]
12151    fn test_vm_try_on_none_propagates() {
12152        let output = run_output(
12153            r#"fn get_none() { none }
12154fn process() { let v = get_none()?
1215542 }
12156print(process())"#,
12157        );
12158        assert_eq!(output, vec!["none"]);
12159    }
12160
12161    #[test]
12162    fn test_vm_try_passthrough() {
12163        // ? on a normal value should passthrough
12164        let output = run_output(
12165            r#"fn get_val() { 42 }
12166fn process() { let v = get_val()?
12167v + 1 }
12168print(process())"#,
12169        );
12170        assert_eq!(output, vec!["43"]);
12171    }
12172
12173    #[test]
12174    fn test_vm_result_match() {
12175        let output = run_output(
12176            r#"let r = Ok(42)
12177print(is_ok(r))
12178print(unwrap(r))"#,
12179        );
12180        assert_eq!(output, vec!["true", "42"]);
12181    }
12182
12183    #[test]
12184    fn test_vm_result_match_err() {
12185        let output = run_output(
12186            r#"let r = Err("fail")
12187print(is_err(r))
12188match r {
12189    Result::Err(e) => print("got error"),
12190    _ => print("no error")
12191}"#,
12192        );
12193        assert_eq!(output, vec!["true", "got error"]);
12194    }
12195
12196    // ── Set tests ──
12197
12198    #[test]
12199    fn test_vm_set_from_dedup() {
12200        let output = run_output(
12201            r#"let s = set_from([1, 2, 3, 2, 1])
12202print(len(s))
12203print(type_of(s))"#,
12204        );
12205        assert_eq!(output, vec!["3", "set"]);
12206    }
12207
12208    #[test]
12209    fn test_vm_set_add() {
12210        let output = run_output(
12211            r#"let s = set_from([1, 2])
12212let s2 = set_add(s, 3)
12213let s3 = set_add(s2, 2)
12214print(len(s2))
12215print(len(s3))"#,
12216        );
12217        assert_eq!(output, vec!["3", "3"]);
12218    }
12219
12220    #[test]
12221    fn test_vm_set_remove() {
12222        let output = run_output(
12223            r#"let s = set_from([1, 2, 3])
12224let s2 = set_remove(s, 2)
12225print(len(s2))
12226print(set_contains(s2, 2))"#,
12227        );
12228        assert_eq!(output, vec!["2", "false"]);
12229    }
12230
12231    #[test]
12232    fn test_vm_set_contains() {
12233        let output = run_output(
12234            r#"let s = set_from([1, 2, 3])
12235print(set_contains(s, 2))
12236print(set_contains(s, 5))"#,
12237        );
12238        assert_eq!(output, vec!["true", "false"]);
12239    }
12240
12241    #[test]
12242    fn test_vm_set_union() {
12243        let output = run_output(
12244            r#"let a = set_from([1, 2, 3])
12245let b = set_from([3, 4, 5])
12246let c = set_union(a, b)
12247print(len(c))"#,
12248        );
12249        assert_eq!(output, vec!["5"]);
12250    }
12251
12252    #[test]
12253    fn test_vm_set_intersection() {
12254        let output = run_output(
12255            r#"let a = set_from([1, 2, 3])
12256let b = set_from([2, 3, 4])
12257let c = set_intersection(a, b)
12258print(len(c))"#,
12259        );
12260        assert_eq!(output, vec!["2"]);
12261    }
12262
12263    #[test]
12264    fn test_vm_set_difference() {
12265        let output = run_output(
12266            r#"let a = set_from([1, 2, 3])
12267let b = set_from([2, 3, 4])
12268let c = set_difference(a, b)
12269print(len(c))"#,
12270        );
12271        assert_eq!(output, vec!["1"]);
12272    }
12273
12274    #[test]
12275    fn test_vm_set_for_loop() {
12276        let output = run_output(
12277            r#"let s = set_from([10, 20, 30])
12278let total = 0
12279for item in s {
12280    total = total + item
12281}
12282print(total)"#,
12283        );
12284        assert_eq!(output, vec!["60"]);
12285    }
12286
12287    #[test]
12288    fn test_vm_set_to_list() {
12289        let output = run_output(
12290            r#"let s = set_from([3, 1, 2])
12291let lst = s.to_list()
12292print(type_of(lst))
12293print(len(lst))"#,
12294        );
12295        assert_eq!(output, vec!["list", "3"]);
12296    }
12297
12298    #[test]
12299    fn test_vm_set_method_contains() {
12300        let output = run_output(
12301            r#"let s = set_from([1, 2, 3])
12302print(s.contains(2))
12303print(s.contains(5))"#,
12304        );
12305        assert_eq!(output, vec!["true", "false"]);
12306    }
12307
12308    #[test]
12309    fn test_vm_set_method_add_remove() {
12310        let output = run_output(
12311            r#"let s = set_from([1, 2])
12312let s2 = s.add(3)
12313print(s2.len())
12314let s3 = s2.remove(1)
12315print(s3.len())"#,
12316        );
12317        assert_eq!(output, vec!["3", "2"]);
12318    }
12319
12320    #[test]
12321    fn test_vm_set_method_union_intersection_difference() {
12322        let output = run_output(
12323            r#"let a = set_from([1, 2, 3])
12324let b = set_from([2, 3, 4])
12325print(a.union(b).len())
12326print(a.intersection(b).len())
12327print(a.difference(b).len())"#,
12328        );
12329        assert_eq!(output, vec!["4", "2", "1"]);
12330    }
12331
12332    #[test]
12333    fn test_vm_set_empty() {
12334        let output = run_output(
12335            r#"let s = set_from([])
12336print(len(s))
12337let s2 = s.add(1)
12338print(len(s2))"#,
12339        );
12340        assert_eq!(output, vec!["0", "1"]);
12341    }
12342
12343    #[test]
12344    fn test_vm_set_string_values() {
12345        let output = run_output(
12346            r#"let s = set_from(["a", "b", "a", "c"])
12347print(len(s))
12348print(s.contains("b"))"#,
12349        );
12350        assert_eq!(output, vec!["3", "true"]);
12351    }
12352
12353    // ── Phase 11: Module System VM Tests ──
12354
12355    #[test]
12356    fn test_vm_import_with_caching() {
12357        // Test that the VM has caching fields initialized
12358        let vm = Vm::new();
12359        assert!(vm.module_cache.is_empty());
12360        assert!(vm.importing_files.is_empty());
12361        assert!(vm.file_path.is_none());
12362    }
12363
12364    #[test]
12365    fn test_vm_use_single_file() {
12366        // Create a temp dir with module files
12367        let dir = tempfile::tempdir().unwrap();
12368        let lib_path = dir.path().join("math.tl");
12369        std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12370
12371        let main_path = dir.path().join("main.tl");
12372        std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12373
12374        let source = std::fs::read_to_string(&main_path).unwrap();
12375        let program = tl_parser::parse(&source).unwrap();
12376        let proto = crate::compiler::compile(&program).unwrap();
12377
12378        let mut vm = Vm::new();
12379        vm.file_path = Some(main_path.to_string_lossy().to_string());
12380        vm.execute(&proto).unwrap();
12381        assert_eq!(vm.output, vec!["3"]);
12382    }
12383
12384    #[test]
12385    fn test_vm_use_wildcard() {
12386        let dir = tempfile::tempdir().unwrap();
12387        std::fs::write(
12388            dir.path().join("helpers.tl"),
12389            "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12390        )
12391        .unwrap();
12392
12393        let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12394        let main_path = dir.path().join("main.tl");
12395        std::fs::write(&main_path, main_src).unwrap();
12396
12397        let program = tl_parser::parse(main_src).unwrap();
12398        let proto = crate::compiler::compile(&program).unwrap();
12399
12400        let mut vm = Vm::new();
12401        vm.file_path = Some(main_path.to_string_lossy().to_string());
12402        vm.execute(&proto).unwrap();
12403        assert_eq!(vm.output, vec!["hello", "bye"]);
12404    }
12405
12406    #[test]
12407    fn test_vm_use_aliased() {
12408        let dir = tempfile::tempdir().unwrap();
12409        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12410
12411        let main_src = "use mylib as m\nprint(m.compute())";
12412        let main_path = dir.path().join("main.tl");
12413        std::fs::write(&main_path, main_src).unwrap();
12414
12415        let program = tl_parser::parse(main_src).unwrap();
12416        let proto = crate::compiler::compile(&program).unwrap();
12417
12418        let mut vm = Vm::new();
12419        vm.file_path = Some(main_path.to_string_lossy().to_string());
12420        vm.execute(&proto).unwrap();
12421        assert_eq!(vm.output, vec!["42"]);
12422    }
12423
12424    #[test]
12425    fn test_vm_use_directory_module() {
12426        let dir = tempfile::tempdir().unwrap();
12427        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12428        std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12429
12430        let main_src = "use utils\nprint(helper())";
12431        let main_path = dir.path().join("main.tl");
12432        std::fs::write(&main_path, main_src).unwrap();
12433
12434        let program = tl_parser::parse(main_src).unwrap();
12435        let proto = crate::compiler::compile(&program).unwrap();
12436
12437        let mut vm = Vm::new();
12438        vm.file_path = Some(main_path.to_string_lossy().to_string());
12439        vm.execute(&proto).unwrap();
12440        assert_eq!(vm.output, vec!["99"]);
12441    }
12442
12443    #[test]
12444    fn test_vm_circular_import_detection() {
12445        let dir = tempfile::tempdir().unwrap();
12446        let a_path = dir.path().join("a.tl");
12447        let b_path = dir.path().join("b.tl");
12448        std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
12449        std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
12450
12451        let source = std::fs::read_to_string(&a_path).unwrap();
12452        let program = tl_parser::parse(&source).unwrap();
12453        let proto = crate::compiler::compile(&program).unwrap();
12454
12455        let mut vm = Vm::new();
12456        vm.file_path = Some(a_path.to_string_lossy().to_string());
12457        let result = vm.execute(&proto);
12458        assert!(result.is_err());
12459        assert!(format!("{:?}", result).contains("Circular import"));
12460    }
12461
12462    #[test]
12463    fn test_vm_module_caching() {
12464        // Import the same module twice — should use cache
12465        let dir = tempfile::tempdir().unwrap();
12466        std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
12467
12468        let main_src = "use cached\nuse cached\nprint(X)";
12469        let main_path = dir.path().join("main.tl");
12470        std::fs::write(&main_path, main_src).unwrap();
12471
12472        let program = tl_parser::parse(main_src).unwrap();
12473        let proto = crate::compiler::compile(&program).unwrap();
12474
12475        let mut vm = Vm::new();
12476        vm.file_path = Some(main_path.to_string_lossy().to_string());
12477        vm.execute(&proto).unwrap();
12478        assert_eq!(vm.output, vec!["42"]);
12479    }
12480
12481    #[test]
12482    fn test_vm_existing_import_still_works() {
12483        // Verify backward compat of classic import
12484        let dir = tempfile::tempdir().unwrap();
12485        let lib_path = dir.path().join("lib.tl");
12486        std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
12487
12488        let main_src = format!(
12489            "import \"{}\"\nprint(imported_fn())",
12490            lib_path.to_string_lossy()
12491        );
12492        let program = tl_parser::parse(&main_src).unwrap();
12493        let proto = crate::compiler::compile(&program).unwrap();
12494
12495        let mut vm = Vm::new();
12496        vm.execute(&proto).unwrap();
12497        assert_eq!(vm.output, vec!["123"]);
12498    }
12499
12500    #[test]
12501    fn test_vm_pub_fn_parsing() {
12502        // Pub fn should compile and run normally
12503        let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
12504        assert_eq!(output, vec!["3"]);
12505    }
12506
12507    #[test]
12508    fn test_vm_use_nested_path() {
12509        let dir = tempfile::tempdir().unwrap();
12510        std::fs::create_dir_all(dir.path().join("data")).unwrap();
12511        std::fs::write(
12512            dir.path().join("data/transforms.tl"),
12513            "fn clean(x) { x + 1 }",
12514        )
12515        .unwrap();
12516
12517        let main_src = "use data.transforms\nprint(clean(41))";
12518        let main_path = dir.path().join("main.tl");
12519        std::fs::write(&main_path, main_src).unwrap();
12520
12521        let program = tl_parser::parse(main_src).unwrap();
12522        let proto = crate::compiler::compile(&program).unwrap();
12523
12524        let mut vm = Vm::new();
12525        vm.file_path = Some(main_path.to_string_lossy().to_string());
12526        vm.execute(&proto).unwrap();
12527        assert_eq!(vm.output, vec!["42"]);
12528    }
12529
12530    // -- Integration tests: multi-file, backward compat, mixed --
12531
12532    #[test]
12533    fn test_integration_multi_file_use_functions() {
12534        // main.tl uses functions from lib.tl
12535        let dir = tempfile::tempdir().unwrap();
12536        std::fs::write(
12537            dir.path().join("lib.tl"),
12538            "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
12539        )
12540        .unwrap();
12541
12542        let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
12543        let main_path = dir.path().join("main.tl");
12544        std::fs::write(&main_path, main_src).unwrap();
12545
12546        let program = tl_parser::parse(main_src).unwrap();
12547        let proto = crate::compiler::compile(&program).unwrap();
12548        let mut vm = Vm::new();
12549        vm.file_path = Some(main_path.to_string_lossy().to_string());
12550        vm.execute(&proto).unwrap();
12551        assert_eq!(vm.output, vec!["Hello, World!", "42"]);
12552    }
12553
12554    #[test]
12555    fn test_integration_mixed_import_and_use() {
12556        // Combine classic import and use in same file
12557        let dir = tempfile::tempdir().unwrap();
12558        std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
12559        std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
12560
12561        let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
12562        let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
12563        let main_path = dir.path().join("main.tl");
12564        std::fs::write(&main_path, &main_src).unwrap();
12565
12566        let program = tl_parser::parse(&main_src).unwrap();
12567        let proto = crate::compiler::compile(&program).unwrap();
12568        let mut vm = Vm::new();
12569        vm.file_path = Some(main_path.to_string_lossy().to_string());
12570        vm.execute(&proto).unwrap();
12571        assert_eq!(vm.output, vec!["30"]);
12572    }
12573
12574    #[test]
12575    fn test_integration_directory_module_with_mod_tl() {
12576        // utils/mod.tl re-exports functions
12577        let dir = tempfile::tempdir().unwrap();
12578        std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12579        std::fs::write(
12580            dir.path().join("utils/mod.tl"),
12581            "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
12582        )
12583        .unwrap();
12584
12585        let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
12586        let main_path = dir.path().join("main.tl");
12587        std::fs::write(&main_path, main_src).unwrap();
12588
12589        let program = tl_parser::parse(main_src).unwrap();
12590        let proto = crate::compiler::compile(&program).unwrap();
12591        let mut vm = Vm::new();
12592        vm.file_path = Some(main_path.to_string_lossy().to_string());
12593        vm.execute(&proto).unwrap();
12594        assert_eq!(vm.output, vec!["99", "42!"]);
12595    }
12596
12597    #[test]
12598    fn test_integration_circular_dep_error() {
12599        let dir = tempfile::tempdir().unwrap();
12600        let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
12601        let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
12602        std::fs::write(
12603            dir.path().join("a.tl"),
12604            format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
12605        )
12606        .unwrap();
12607        std::fs::write(
12608            dir.path().join("b.tl"),
12609            format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
12610        )
12611        .unwrap();
12612
12613        let main_src = format!("import \"{a_abs}\"");
12614        let program = tl_parser::parse(&main_src).unwrap();
12615        let proto = crate::compiler::compile(&program).unwrap();
12616        let mut vm = Vm::new();
12617        let result = vm.execute(&proto);
12618        assert!(result.is_err());
12619        let err_msg = format!("{}", result.unwrap_err());
12620        assert!(
12621            err_msg.contains("Circular") || err_msg.contains("circular"),
12622            "Expected circular import error, got: {err_msg}"
12623        );
12624    }
12625
12626    #[test]
12627    fn test_integration_use_aliased_method_call() {
12628        // use lib as m, then m.compute()
12629        let dir = tempfile::tempdir().unwrap();
12630        std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12631
12632        let main_src = "use mylib as m\nprint(m.compute())";
12633        let main_path = dir.path().join("main.tl");
12634        std::fs::write(&main_path, main_src).unwrap();
12635
12636        let program = tl_parser::parse(main_src).unwrap();
12637        let proto = crate::compiler::compile(&program).unwrap();
12638        let mut vm = Vm::new();
12639        vm.file_path = Some(main_path.to_string_lossy().to_string());
12640        vm.execute(&proto).unwrap();
12641        assert_eq!(vm.output, vec!["42"]);
12642    }
12643
12644    #[test]
12645    fn test_integration_module_caching_shared() {
12646        // Import same module twice; second import uses cache, not re-execution
12647        let dir = tempfile::tempdir().unwrap();
12648        std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
12649
12650        let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
12651        let main_path = dir.path().join("main.tl");
12652        std::fs::write(&main_path, main_src).unwrap();
12653
12654        let program = tl_parser::parse(main_src).unwrap();
12655        let proto = crate::compiler::compile(&program).unwrap();
12656        let mut vm = Vm::new();
12657        vm.file_path = Some(main_path.to_string_lossy().to_string());
12658        vm.execute(&proto).unwrap();
12659        assert_eq!(vm.output, vec!["42", "42"]);
12660    }
12661
12662    #[test]
12663    fn test_integration_pub_keyword_in_module() {
12664        // pub fn in a module should work when imported
12665        let dir = tempfile::tempdir().unwrap();
12666        std::fs::write(
12667            dir.path().join("pubmod.tl"),
12668            "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
12669        )
12670        .unwrap();
12671
12672        let main_src = "use pubmod\nprint(public_fn())";
12673        let main_path = dir.path().join("main.tl");
12674        std::fs::write(&main_path, main_src).unwrap();
12675
12676        let program = tl_parser::parse(main_src).unwrap();
12677        let proto = crate::compiler::compile(&program).unwrap();
12678        let mut vm = Vm::new();
12679        vm.file_path = Some(main_path.to_string_lossy().to_string());
12680        vm.execute(&proto).unwrap();
12681        assert_eq!(vm.output, vec!["100"]);
12682    }
12683
12684    #[test]
12685    fn test_integration_backward_compat_import_as() {
12686        // Classic import-as syntax should still work
12687        let dir = tempfile::tempdir().unwrap();
12688        let lib_path = dir.path().join("mylib.tl");
12689        std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
12690
12691        let main_src = format!(
12692            "import \"{}\" as m\nprint(m.compute())",
12693            lib_path.to_string_lossy()
12694        );
12695        let program = tl_parser::parse(&main_src).unwrap();
12696        let proto = crate::compiler::compile(&program).unwrap();
12697        let mut vm = Vm::new();
12698        vm.execute(&proto).unwrap();
12699        assert_eq!(vm.output, vec!["77"]);
12700    }
12701
12702    // ── Phase 12: Generics & Traits (VM) ──────────────────
12703
12704    #[test]
12705    fn test_vm_generic_fn() {
12706        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
12707        assert_eq!(output, vec!["42"]);
12708    }
12709
12710    #[test]
12711    fn test_vm_generic_fn_string() {
12712        let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
12713        assert_eq!(output, vec!["hello"]);
12714    }
12715
12716    #[test]
12717    fn test_vm_generic_struct() {
12718        let output = run_output(
12719            "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
12720        );
12721        assert_eq!(output, vec!["1", "hi"]);
12722    }
12723
12724    #[test]
12725    fn test_vm_trait_def_noop() {
12726        // Trait definitions should compile without error (no-op)
12727        let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
12728        assert_eq!(output, vec!["ok"]);
12729    }
12730
12731    #[test]
12732    fn test_vm_trait_impl_methods() {
12733        let output = run_output(
12734            "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())",
12735        );
12736        assert_eq!(output, vec!["point"]);
12737    }
12738
12739    #[test]
12740    fn test_vm_generic_enum() {
12741        // Generic enum declaration works — type params are erased at runtime
12742        let output = run_output(
12743            "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
12744        );
12745        assert_eq!(output, vec!["enum"]);
12746    }
12747
12748    #[test]
12749    fn test_vm_where_clause_runtime() {
12750        // Where clause is compile-time only; function still works at runtime
12751        let output =
12752            run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
12753        assert_eq!(output, vec!["10"]);
12754    }
12755
12756    #[test]
12757    fn test_vm_trait_impl_self_method() {
12758        let output = run_output(
12759            "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
12760        );
12761        assert_eq!(output, vec!["6"]);
12762    }
12763
12764    // ── Phase 12: Integration tests ──────────────────────────
12765
12766    #[test]
12767    fn test_vm_generic_fn_with_type_inference() {
12768        // Generic function called with different types
12769        let output = run_output(
12770            "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
12771        );
12772        assert_eq!(output, vec!["1", "a"]);
12773    }
12774
12775    #[test]
12776    fn test_vm_generic_struct_with_methods() {
12777        let output = run_output(
12778            "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
12779        );
12780        assert_eq!(output, vec!["42"]);
12781    }
12782
12783    #[test]
12784    fn test_vm_trait_def_impl_call() {
12785        let output = run_output(
12786            "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())",
12787        );
12788        assert_eq!(output, vec!["Alice"]);
12789    }
12790
12791    #[test]
12792    fn test_vm_multiple_generic_params() {
12793        let output = run_output(
12794            "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
12795        );
12796        assert_eq!(output, vec!["2"]);
12797    }
12798
12799    #[test]
12800    fn test_vm_backward_compat_non_generic() {
12801        // Existing non-generic code must still work unchanged
12802        let output = run_output(
12803            "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())",
12804        );
12805        assert_eq!(output, vec!["3", "7"]);
12806    }
12807
12808    // ── Phase 16: Package import resolution tests ──
12809
12810    #[test]
12811    fn test_vm_package_import_resolves() {
12812        // Create a test package on disk
12813        let tmp = tempfile::tempdir().unwrap();
12814        let pkg_dir = tmp.path().join("mylib");
12815        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12816        std::fs::write(
12817            pkg_dir.join("src/lib.tl"),
12818            "pub fn greet() { print(\"hello from pkg\") }",
12819        )
12820        .unwrap();
12821        std::fs::write(
12822            pkg_dir.join("tl.toml"),
12823            "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
12824        )
12825        .unwrap();
12826
12827        // use X imports all exports wildcard-style; call greet() directly
12828        let main_file = tmp.path().join("main.tl");
12829        std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
12830
12831        let source = std::fs::read_to_string(&main_file).unwrap();
12832        let program = tl_parser::parse(&source).unwrap();
12833        let proto = crate::compiler::compile(&program).unwrap();
12834
12835        let mut vm = Vm::new();
12836        vm.file_path = Some(main_file.to_string_lossy().to_string());
12837        vm.package_roots.insert("mylib".into(), pkg_dir);
12838        vm.execute(&proto).unwrap();
12839
12840        assert_eq!(vm.output, vec!["hello from pkg"]);
12841    }
12842
12843    #[test]
12844    fn test_vm_package_nested_import() {
12845        let tmp = tempfile::tempdir().unwrap();
12846        let pkg_dir = tmp.path().join("utils");
12847        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12848        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12849        std::fs::write(
12850            pkg_dir.join("tl.toml"),
12851            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12852        )
12853        .unwrap();
12854
12855        // use utils.math wildcard-imports math.tl contents
12856        let main_file = tmp.path().join("main.tl");
12857        std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
12858
12859        let source = std::fs::read_to_string(&main_file).unwrap();
12860        let program = tl_parser::parse(&source).unwrap();
12861        let proto = crate::compiler::compile(&program).unwrap();
12862
12863        let mut vm = Vm::new();
12864        vm.file_path = Some(main_file.to_string_lossy().to_string());
12865        vm.package_roots.insert("utils".into(), pkg_dir);
12866        vm.execute(&proto).unwrap();
12867
12868        assert_eq!(vm.output, vec!["42"]);
12869    }
12870
12871    #[test]
12872    fn test_vm_package_aliased_import() {
12873        let tmp = tempfile::tempdir().unwrap();
12874        let pkg_dir = tmp.path().join("utils");
12875        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12876        std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
12877        std::fs::write(
12878            pkg_dir.join("tl.toml"),
12879            "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
12880        )
12881        .unwrap();
12882
12883        // use X as Y creates a namespaced module object
12884        let main_file = tmp.path().join("main.tl");
12885        std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
12886
12887        let source = std::fs::read_to_string(&main_file).unwrap();
12888        let program = tl_parser::parse(&source).unwrap();
12889        let proto = crate::compiler::compile(&program).unwrap();
12890
12891        let mut vm = Vm::new();
12892        vm.file_path = Some(main_file.to_string_lossy().to_string());
12893        vm.package_roots.insert("utils".into(), pkg_dir);
12894        vm.execute(&proto).unwrap();
12895
12896        assert_eq!(vm.output, vec!["42"]);
12897    }
12898
12899    #[test]
12900    fn test_vm_package_underscore_to_hyphen() {
12901        let tmp = tempfile::tempdir().unwrap();
12902        let pkg_dir = tmp.path().join("my-pkg");
12903        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12904        std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
12905        std::fs::write(
12906            pkg_dir.join("tl.toml"),
12907            "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
12908        )
12909        .unwrap();
12910
12911        // TL identifiers use underscores, package names use hyphens
12912        let main_file = tmp.path().join("main.tl");
12913        std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
12914
12915        let source = std::fs::read_to_string(&main_file).unwrap();
12916        let program = tl_parser::parse(&source).unwrap();
12917        let proto = crate::compiler::compile(&program).unwrap();
12918
12919        let mut vm = Vm::new();
12920        vm.file_path = Some(main_file.to_string_lossy().to_string());
12921        vm.package_roots.insert("my-pkg".into(), pkg_dir);
12922        vm.execute(&proto).unwrap();
12923
12924        assert_eq!(vm.output, vec!["99"]);
12925    }
12926
12927    #[test]
12928    fn test_vm_local_module_priority_over_package() {
12929        // Local modules should take priority over packages
12930        let tmp = tempfile::tempdir().unwrap();
12931
12932        // Create a local module
12933        std::fs::write(
12934            tmp.path().join("mymod.tl"),
12935            "pub fn val() { print(\"local\") }",
12936        )
12937        .unwrap();
12938
12939        // Create a package with the same name
12940        let pkg_dir = tmp.path().join("pkg_mymod");
12941        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
12942        std::fs::write(
12943            pkg_dir.join("src/lib.tl"),
12944            "pub fn val() { print(\"package\") }",
12945        )
12946        .unwrap();
12947
12948        // use mymod → wildcard imports, val() available directly
12949        let main_file = tmp.path().join("main.tl");
12950        std::fs::write(&main_file, "use mymod\nval()").unwrap();
12951
12952        let source = std::fs::read_to_string(&main_file).unwrap();
12953        let program = tl_parser::parse(&source).unwrap();
12954        let proto = crate::compiler::compile(&program).unwrap();
12955
12956        let mut vm = Vm::new();
12957        vm.file_path = Some(main_file.to_string_lossy().to_string());
12958        vm.package_roots.insert("mymod".into(), pkg_dir);
12959        vm.execute(&proto).unwrap();
12960
12961        // Local module should win
12962        assert_eq!(vm.output, vec!["local"]);
12963    }
12964
12965    #[test]
12966    fn test_vm_package_missing_error() {
12967        let tmp = tempfile::tempdir().unwrap();
12968        let main_file = tmp.path().join("main.tl");
12969        std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
12970
12971        let source = std::fs::read_to_string(&main_file).unwrap();
12972        let program = tl_parser::parse(&source).unwrap();
12973        let proto = crate::compiler::compile(&program).unwrap();
12974
12975        let mut vm = Vm::new();
12976        vm.file_path = Some(main_file.to_string_lossy().to_string());
12977        let result = vm.execute(&proto);
12978
12979        assert!(result.is_err());
12980        let err = format!("{:?}", result.unwrap_err());
12981        assert!(err.contains("Module not found"));
12982    }
12983
12984    #[test]
12985    #[cfg(feature = "native")]
12986    fn test_resolve_package_file_entry_points() {
12987        let tmp = tempfile::tempdir().unwrap();
12988
12989        // Test src/lib.tl entry point
12990        std::fs::create_dir_all(tmp.path().join("src")).unwrap();
12991        std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
12992        let result = resolve_package_file(tmp.path(), &[]);
12993        assert!(result.is_some());
12994        assert!(result.unwrap().contains("lib.tl"));
12995
12996        // Test nested module in src/
12997        std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
12998        let result = resolve_package_file(tmp.path(), &["math"]);
12999        assert!(result.is_some());
13000        assert!(result.unwrap().contains("math.tl"));
13001    }
13002
13003    #[test]
13004    fn test_vm_package_propagates_to_sub_imports() {
13005        // Package roots should be available in sub-VM during imports
13006        let tmp = tempfile::tempdir().unwrap();
13007
13008        // Create a package
13009        let pkg_dir = tmp.path().join("helpers");
13010        std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13011        std::fs::write(
13012            pkg_dir.join("src/lib.tl"),
13013            "pub fn help() { print(\"helped\") }",
13014        )
13015        .unwrap();
13016        std::fs::write(
13017            pkg_dir.join("tl.toml"),
13018            "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13019        )
13020        .unwrap();
13021
13022        // Create a local module that imports from the package (wildcard then calls directly)
13023        std::fs::write(
13024            tmp.path().join("bridge.tl"),
13025            "use helpers\npub fn run() { help() }",
13026        )
13027        .unwrap();
13028
13029        // use bridge wildcard-imports run(), then call it
13030        let main_file = tmp.path().join("main.tl");
13031        std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13032
13033        let source = std::fs::read_to_string(&main_file).unwrap();
13034        let program = tl_parser::parse(&source).unwrap();
13035        let proto = crate::compiler::compile(&program).unwrap();
13036
13037        let mut vm = Vm::new();
13038        vm.file_path = Some(main_file.to_string_lossy().to_string());
13039        vm.package_roots.insert("helpers".into(), pkg_dir);
13040        vm.execute(&proto).unwrap();
13041
13042        assert_eq!(vm.output, vec!["helped"]);
13043    }
13044
13045    // ── Phase 18: Closures & Lambdas Improvements ────────────────
13046
13047    #[test]
13048    fn test_block_body_closure_basic() {
13049        let output =
13050            run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13051        assert_eq!(output, vec!["11"]);
13052    }
13053
13054    #[test]
13055    fn test_block_body_closure_captures_upvalue() {
13056        let output = run_output(
13057            "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13058        );
13059        assert_eq!(output, vec!["15"]);
13060    }
13061
13062    #[test]
13063    fn test_block_body_closure_as_hof_arg() {
13064        let output = run_output(
13065            "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13066        );
13067        assert_eq!(output, vec!["[3, 5, 7]"]);
13068    }
13069
13070    #[test]
13071    fn test_block_body_closure_multi_stmt() {
13072        let output = run_output(
13073            "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13074        );
13075        assert_eq!(output, vec!["19"]);
13076    }
13077
13078    #[test]
13079    fn test_type_alias_noop() {
13080        // Type alias should be a no-op at runtime, code using aliased types should work
13081        let output = run_output(
13082            "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13083        );
13084        assert_eq!(output, vec!["10"]);
13085    }
13086
13087    #[test]
13088    fn test_type_alias_in_function_sig() {
13089        let output = run_output(
13090            "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13091        );
13092        assert_eq!(output, vec!["15"]);
13093    }
13094
13095    #[test]
13096    fn test_shorthand_closure() {
13097        let output = run_output("let double = x => x * 2\nprint(double(5))");
13098        assert_eq!(output, vec!["10"]);
13099    }
13100
13101    #[test]
13102    fn test_shorthand_closure_in_map() {
13103        let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13104        assert_eq!(output, vec!["[2, 4, 6]"]);
13105    }
13106
13107    #[test]
13108    fn test_iife() {
13109        let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13110        assert_eq!(output, vec!["10"]);
13111    }
13112
13113    #[test]
13114    fn test_hof_apply() {
13115        let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13116        assert_eq!(output, vec!["15"]);
13117    }
13118
13119    #[test]
13120    fn test_closure_stored_in_list() {
13121        let output = run_output(
13122            "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13123        );
13124        assert_eq!(output, vec!["6", "10"]);
13125    }
13126
13127    #[test]
13128    fn test_block_body_closure_with_return() {
13129        // Use explicit return statements since if/else is a statement, not a tail expression
13130        let output = run_output(
13131            "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13132        );
13133        assert_eq!(output, vec!["positive", "non-positive"]);
13134    }
13135
13136    #[test]
13137    fn test_shorthand_closure_in_filter() {
13138        let output = run_output(
13139            "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13140        );
13141        assert_eq!(output, vec!["[2, 4, 6]"]);
13142    }
13143
13144    #[test]
13145    fn test_block_closure_with_multiple_returns() {
13146        let output = run_output(
13147            "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13148        );
13149        assert_eq!(output, vec!["5", "3"]);
13150    }
13151
13152    #[test]
13153    fn test_type_alias_with_block_closure() {
13154        let output = run_output(
13155            "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13156        );
13157        assert_eq!(output, vec!["10"]);
13158    }
13159
13160    #[test]
13161    fn test_closure_both_backends_expr() {
13162        // Same test, just verify VM works correctly
13163        let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13164        assert_eq!(output, vec!["13"]);
13165    }
13166
13167    // Phase 20: Python FFI feature-disabled test
13168    #[test]
13169    #[cfg(not(feature = "python"))]
13170    fn test_py_feature_disabled() {
13171        let result = run("py_import(\"math\")");
13172        assert!(result.is_err());
13173        let msg = format!("{}", result.unwrap_err());
13174        assert!(msg.contains("python") && msg.contains("feature"));
13175    }
13176
13177    #[test]
13178    #[cfg(feature = "python")]
13179    fn test_vm_py_import_and_eval() {
13180        pyo3::prepare_freethreaded_python();
13181        let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13182        assert_eq!(output.len(), 1);
13183        let pi: f64 = output[0].parse().unwrap();
13184        assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13185    }
13186
13187    #[test]
13188    #[cfg(feature = "python")]
13189    fn test_vm_py_eval_arithmetic() {
13190        pyo3::prepare_freethreaded_python();
13191        let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13192        assert_eq!(output, vec!["1024"]);
13193    }
13194
13195    #[test]
13196    #[cfg(feature = "python")]
13197    fn test_vm_py_method_dispatch() {
13198        pyo3::prepare_freethreaded_python();
13199        let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13200        assert_eq!(output, vec!["5.0"]);
13201    }
13202
13203    #[test]
13204    #[cfg(feature = "python")]
13205    fn test_vm_py_list_conversion() {
13206        pyo3::prepare_freethreaded_python();
13207        let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13208        assert_eq!(output, vec!["[10, 20, 30]"]);
13209    }
13210
13211    #[test]
13212    #[cfg(feature = "python")]
13213    fn test_vm_py_none_conversion() {
13214        pyo3::prepare_freethreaded_python();
13215        let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13216        assert_eq!(output, vec!["none"]);
13217    }
13218
13219    #[test]
13220    #[cfg(feature = "python")]
13221    fn test_vm_py_error_msg_quality() {
13222        pyo3::prepare_freethreaded_python();
13223        let result = run("py_import(\"nonexistent_xyz_module\")");
13224        assert!(result.is_err());
13225        let msg = format!("{}", result.unwrap_err());
13226        assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13227    }
13228
13229    #[test]
13230    #[cfg(feature = "python")]
13231    fn test_vm_py_getattr_setattr() {
13232        pyo3::prepare_freethreaded_python();
13233        let output = run_output(
13234            "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13235        );
13236        assert_eq!(output, vec!["99"]);
13237    }
13238
13239    #[test]
13240    #[cfg(feature = "python")]
13241    fn test_vm_py_callable_round_trip() {
13242        pyo3::prepare_freethreaded_python();
13243        let output = run_output(
13244            "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13245        );
13246        assert_eq!(output, vec!["3"]);
13247    }
13248
13249    // ── Phase 21: Schema Evolution VM tests ──
13250
13251    #[test]
13252    fn test_vm_schema_register_and_get() {
13253        let source = r#"let fields = map_from("id", "int64", "name", "string")
13254schema_register("User", 1, fields)
13255let result = schema_get("User", 1)
13256print(len(result))"#;
13257        let output = run_output(source);
13258        assert_eq!(output, vec!["2"]);
13259    }
13260
13261    #[test]
13262    fn test_vm_schema_latest() {
13263        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13264schema_register("User", 2, map_from("id", "int64", "name", "string"))
13265let latest = schema_latest("User")
13266print(latest)"#;
13267        let output = run_output(source);
13268        assert_eq!(output, vec!["2"]);
13269    }
13270
13271    #[test]
13272    fn test_vm_schema_history() {
13273        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13274schema_register("User", 2, map_from("id", "int64", "name", "string"))
13275let hist = schema_history("User")
13276print(len(hist))"#;
13277        let output = run_output(source);
13278        assert_eq!(output, vec!["2"]);
13279    }
13280
13281    #[test]
13282    fn test_vm_schema_check_backward_compat() {
13283        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13284schema_register("User", 2, map_from("id", "int64", "name", "string"))
13285let issues = schema_check("User", 1, 2, "backward")
13286print(len(issues))"#;
13287        let output = run_output(source);
13288        assert_eq!(output, vec!["0"]);
13289    }
13290
13291    #[test]
13292    fn test_vm_schema_diff() {
13293        let source = r#"schema_register("User", 1, map_from("id", "int64"))
13294schema_register("User", 2, map_from("id", "int64", "name", "string"))
13295let diffs = schema_diff("User", 1, 2)
13296print(len(diffs))"#;
13297        let output = run_output(source);
13298        assert_eq!(output, vec!["1"]);
13299    }
13300
13301    #[test]
13302    fn test_vm_schema_versions() {
13303        let source = r#"schema_register("T", 1, map_from("id", "int64"))
13304schema_register("T", 3, map_from("id", "int64"))
13305schema_register("T", 2, map_from("id", "int64"))
13306let vers = schema_versions("T")
13307print(len(vers))"#;
13308        let output = run_output(source);
13309        assert_eq!(output, vec!["3"]);
13310    }
13311
13312    #[test]
13313    fn test_vm_schema_fields() {
13314        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13315let fields = schema_fields("User", 1)
13316print(len(fields))"#;
13317        let output = run_output(source);
13318        assert_eq!(output, vec!["2"]);
13319    }
13320
13321    #[test]
13322    fn test_vm_compile_versioned_schema() {
13323        let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13324        let output = run_output(source);
13325        assert!(output[0].contains("__schema__:User:v1:"));
13326    }
13327
13328    #[test]
13329    fn test_vm_compile_migrate() {
13330        let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13331        let output = run_output(source);
13332        assert_eq!(output, vec!["ok"]);
13333    }
13334
13335    #[test]
13336    fn test_vm_schema_check_backward_compat_fails() {
13337        let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13338schema_register("User", 2, map_from("id", "int64"))
13339let issues = schema_check("User", 1, 2, "backward")
13340print(len(issues))"#;
13341        let output = run_output(source);
13342        assert_eq!(output, vec!["1"]);
13343    }
13344
13345    // ── Phase 22: Decimal VM Tests ─────────────────────────────────
13346
13347    #[test]
13348    fn test_vm_decimal_literal_and_arithmetic() {
13349        let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13350        assert_eq!(output, vec!["13.0", "26.25"]);
13351    }
13352
13353    #[test]
13354    fn test_vm_decimal_div_by_zero() {
13355        let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13356        let program = tl_parser::parse(source).unwrap();
13357        let proto = crate::compile(&program).unwrap();
13358        let mut vm = Vm::new();
13359        let result = vm.execute(&proto);
13360        assert!(result.is_err());
13361    }
13362
13363    #[test]
13364    fn test_vm_decimal_comparison_ops() {
13365        let output =
13366            run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13367        assert_eq!(output, vec!["true", "false", "true"]);
13368    }
13369
13370    // ── Phase 23: Security VM Tests ────────────────────────────────
13371
13372    #[test]
13373    fn test_vm_secret_vault_crud() {
13374        let output = run_output(
13375            "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13376        );
13377        assert_eq!(output, vec!["***", "none"]);
13378    }
13379
13380    #[test]
13381    fn test_vm_mask_email_basic() {
13382        let output = run_output("print(mask_email(\"alice@domain.com\"))");
13383        assert_eq!(output, vec!["a***@domain.com"]);
13384    }
13385
13386    #[test]
13387    fn test_vm_mask_phone_basic() {
13388        let output = run_output("print(mask_phone(\"123-456-7890\"))");
13389        assert_eq!(output, vec!["***-***-7890"]);
13390    }
13391
13392    #[test]
13393    fn test_vm_mask_cc_basic() {
13394        let output = run_output("print(mask_cc(\"4111222233334444\"))");
13395        assert_eq!(output, vec!["****-****-****-4444"]);
13396    }
13397
13398    #[test]
13399    fn test_vm_hash_produces_hex() {
13400        let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13401        assert_eq!(output, vec!["64"]);
13402    }
13403
13404    #[test]
13405    fn test_vm_redact_modes() {
13406        let output =
13407            run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13408        assert_eq!(output, vec!["***", "h***o"]);
13409    }
13410
13411    #[test]
13412    fn test_vm_security_policy_sandbox() {
13413        let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13414        let program = tl_parser::parse(source).unwrap();
13415        let proto = crate::compile(&program).unwrap();
13416        let mut vm = Vm::new();
13417        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13418        vm.execute(&proto).unwrap();
13419        assert_eq!(vm.output, vec!["false", "true"]);
13420    }
13421
13422    // ── Phase 25: Async Runtime Tests (feature-gated) ──────────────
13423
13424    #[cfg(feature = "async-runtime")]
13425    #[test]
13426    fn test_vm_async_read_write_file() {
13427        let dir = tempfile::tempdir().unwrap();
13428        let path = dir.path().join("async_test.txt");
13429        let path_str = path.to_str().unwrap().replace('\\', "/");
13430        let source = format!(
13431            r#"let wt = async_write_file("{path_str}", "async hello")
13432let wr = await(wt)
13433let rt = async_read_file("{path_str}")
13434let content = await(rt)
13435print(content)"#
13436        );
13437        let output = run_output(&source);
13438        assert_eq!(output, vec!["async hello"]);
13439    }
13440
13441    #[cfg(feature = "async-runtime")]
13442    #[test]
13443    fn test_vm_async_sleep() {
13444        let source = r#"
13445let t = async_sleep(10)
13446let r = await(t)
13447print(r)
13448"#;
13449        let output = run_output(source);
13450        assert_eq!(output, vec!["none"]);
13451    }
13452
13453    #[cfg(feature = "async-runtime")]
13454    #[test]
13455    fn test_vm_select_first_wins() {
13456        // select between a fast sleep and a slow sleep — fast one wins
13457        let source = r#"
13458let fast = async_sleep(10)
13459let slow = async_sleep(5000)
13460let winner = select(fast, slow)
13461let result = await(winner)
13462print(result)
13463"#;
13464        let output = run_output(source);
13465        assert_eq!(output, vec!["none"]);
13466    }
13467
13468    #[cfg(feature = "async-runtime")]
13469    #[test]
13470    fn test_vm_race_all() {
13471        let source = r#"
13472let t1 = async_sleep(10)
13473let t2 = async_sleep(5000)
13474let winner = race_all([t1, t2])
13475let result = await(winner)
13476print(result)
13477"#;
13478        let output = run_output(source);
13479        assert_eq!(output, vec!["none"]);
13480    }
13481
13482    #[cfg(feature = "async-runtime")]
13483    #[test]
13484    fn test_vm_async_map() {
13485        let source = r#"
13486let items = [1, 2, 3]
13487let t = async_map(items, (x) => x * 10)
13488let result = await(t)
13489print(result)
13490"#;
13491        let output = run_output(source);
13492        assert_eq!(output, vec!["[10, 20, 30]"]);
13493    }
13494
13495    #[cfg(feature = "async-runtime")]
13496    #[test]
13497    fn test_vm_async_filter() {
13498        let source = r#"
13499let items = [1, 2, 3, 4, 5]
13500let t = async_filter(items, (x) => x > 3)
13501let result = await(t)
13502print(result)
13503"#;
13504        let output = run_output(source);
13505        assert_eq!(output, vec!["[4, 5]"]);
13506    }
13507
13508    #[cfg(feature = "async-runtime")]
13509    #[test]
13510    fn test_vm_async_write_file_returns_none() {
13511        let dir = tempfile::tempdir().unwrap();
13512        let path = dir.path().join("write_test.txt");
13513        let path_str = path.to_str().unwrap().replace('\\', "/");
13514        let source = format!(
13515            r#"let t = async_write_file("{path_str}", "test data")
13516let r = await(t)
13517print(r)"#
13518        );
13519        let output = run_output(&source);
13520        assert_eq!(output, vec!["none"]);
13521    }
13522
13523    #[cfg(feature = "async-runtime")]
13524    #[test]
13525    fn test_vm_async_security_policy_blocks_write() {
13526        let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
13527        let program = tl_parser::parse(source).unwrap();
13528        let proto = crate::compile(&program).unwrap();
13529        let mut vm = Vm::new();
13530        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13531        let result = vm.execute(&proto);
13532        assert!(result.is_err());
13533        let err = format!("{}", result.unwrap_err());
13534        assert!(
13535            err.contains("file_write not allowed"),
13536            "Expected security error, got: {err}"
13537        );
13538    }
13539
13540    #[cfg(feature = "async-runtime")]
13541    #[test]
13542    fn test_vm_async_security_policy_allows_read() {
13543        // Sandbox allows file_read, so async_read_file should succeed (even if file doesn't exist)
13544        let dir = tempfile::tempdir().unwrap();
13545        let path = dir.path().join("readable.txt");
13546        std::fs::write(&path, "safe content").unwrap();
13547        let path_str = path.to_str().unwrap().replace('\\', "/");
13548        let source = format!(
13549            r#"let t = async_read_file("{path_str}")
13550let r = await(t)
13551print(r)"#
13552        );
13553        let program = tl_parser::parse(&source).unwrap();
13554        let proto = crate::compile(&program).unwrap();
13555        let mut vm = Vm::new();
13556        vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13557        vm.execute(&proto).unwrap();
13558        assert_eq!(vm.output, vec!["safe content"]);
13559    }
13560
13561    #[cfg(feature = "async-runtime")]
13562    #[test]
13563    fn test_vm_async_map_empty_list() {
13564        let source = r#"
13565let t = async_map([], (x) => x * 2)
13566let result = await(t)
13567print(result)
13568"#;
13569        let output = run_output(source);
13570        assert_eq!(output, vec!["[]"]);
13571    }
13572
13573    #[cfg(feature = "async-runtime")]
13574    #[test]
13575    fn test_vm_async_filter_none_match() {
13576        let source = r#"
13577let t = async_filter([1, 2, 3], (x) => x > 100)
13578let result = await(t)
13579print(result)
13580"#;
13581        let output = run_output(source);
13582        assert_eq!(output, vec!["[]"]);
13583    }
13584
13585    // --- Phase 26: Closure upvalue closing tests ---
13586
13587    #[test]
13588    fn test_vm_closure_returned_from_function() {
13589        let output = run_output(
13590            r#"
13591fn make_adder(n) {
13592    return (x) => x + n
13593}
13594let add5 = make_adder(5)
13595print(add5(3))
13596print(add5(10))
13597"#,
13598        );
13599        assert_eq!(output, vec!["8", "15"]);
13600    }
13601
13602    #[test]
13603    fn test_vm_closure_factory_multiple_calls() {
13604        let output = run_output(
13605            r#"
13606fn make_adder(n) {
13607    return (x) => x + n
13608}
13609let add2 = make_adder(2)
13610let add10 = make_adder(10)
13611print(add2(5))
13612print(add10(5))
13613print(add2(1))
13614"#,
13615        );
13616        assert_eq!(output, vec!["7", "15", "3"]);
13617    }
13618
13619    #[test]
13620    fn test_vm_closure_returned_in_list() {
13621        let output = run_output(
13622            r#"
13623fn make_ops(n) {
13624    let add = (x) => x + n
13625    let mul = (x) => x * n
13626    return [add, mul]
13627}
13628let ops = make_ops(3)
13629print(ops[0](10))
13630print(ops[1](10))
13631"#,
13632        );
13633        assert_eq!(output, vec!["13", "30"]);
13634    }
13635
13636    #[test]
13637    fn test_vm_nested_closure_return() {
13638        let output = run_output(
13639            r#"
13640fn outer(a) {
13641    fn inner(b) {
13642        return (x) => x + a + b
13643    }
13644    return inner(10)
13645}
13646let f = outer(5)
13647print(f(1))
13648"#,
13649        );
13650        assert_eq!(output, vec!["16"]);
13651    }
13652
13653    #[test]
13654    fn test_vm_multiple_closures_same_local() {
13655        let output = run_output(
13656            r#"
13657fn make_pair(n) {
13658    let inc = (x) => x + n
13659    let dec = (x) => x - n
13660    return [inc, dec]
13661}
13662let pair = make_pair(7)
13663print(pair[0](10))
13664print(pair[1](10))
13665"#,
13666        );
13667        assert_eq!(output, vec!["17", "3"]);
13668    }
13669
13670    #[test]
13671    fn test_vm_closure_captures_multiple_locals() {
13672        let output = run_output(
13673            r#"
13674fn make_greeter(greeting, name) {
13675    let sep = " "
13676    return () => greeting + sep + name
13677}
13678let hi = make_greeter("Hello", "World")
13679let bye = make_greeter("Goodbye", "Alice")
13680print(hi())
13681print(bye())
13682"#,
13683        );
13684        assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
13685    }
13686
13687    // ── Phase 27: Data Error Hierarchy tests ──
13688
13689    #[test]
13690    fn test_vm_throw_catch_preserves_enum() {
13691        let output = run_output(
13692            r#"
13693enum Color { Red, Green(x) }
13694try {
13695    throw Color::Green(42)
13696} catch e {
13697    match e {
13698        Color::Green(x) => print(x),
13699        _ => print("no match"),
13700    }
13701}
13702"#,
13703        );
13704        assert_eq!(output, vec!["42"]);
13705    }
13706
13707    #[test]
13708    fn test_vm_throw_catch_string_compat() {
13709        let output = run_output(
13710            r#"
13711try {
13712    throw "hello error"
13713} catch e {
13714    print(e)
13715}
13716"#,
13717        );
13718        assert_eq!(output, vec!["hello error"]);
13719    }
13720
13721    #[test]
13722    fn test_vm_runtime_error_still_string() {
13723        let output = run_output(
13724            r#"
13725try {
13726    let x = 1 / 0
13727} catch e {
13728    print(type_of(e))
13729}
13730"#,
13731        );
13732        assert_eq!(output, vec!["string"]);
13733    }
13734
13735    #[test]
13736    fn test_vm_data_error_construct_and_throw() {
13737        let output = run_output(
13738            r#"
13739try {
13740    throw DataError::ParseError("bad format", "file.csv")
13741} catch e {
13742    print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
13743    print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
13744}
13745"#,
13746        );
13747        assert_eq!(output, vec!["bad format", "file.csv"]);
13748    }
13749
13750    #[test]
13751    fn test_vm_network_error_construct() {
13752        let output = run_output(
13753            r#"
13754let err = NetworkError::TimeoutError("timed out")
13755match err {
13756    NetworkError::TimeoutError(msg) => print(msg),
13757    _ => print("no match"),
13758}
13759"#,
13760        );
13761        assert_eq!(output, vec!["timed out"]);
13762    }
13763
13764    #[test]
13765    fn test_vm_connector_error_construct() {
13766        let output = run_output(
13767            r#"
13768let err = ConnectorError::AuthError("invalid creds", "postgres")
13769print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
13770print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
13771"#,
13772        );
13773        assert_eq!(output, vec!["invalid creds", "postgres"]);
13774    }
13775
13776    #[test]
13777    fn test_vm_is_error_builtin() {
13778        let output = run_output(
13779            r#"
13780let e1 = DataError::NotFound("users")
13781let e2 = NetworkError::TimeoutError("slow")
13782let e3 = ConnectorError::ConfigError("bad", "redis")
13783let e4 = "not an error"
13784print(is_error(e1))
13785print(is_error(e2))
13786print(is_error(e3))
13787print(is_error(e4))
13788"#,
13789        );
13790        assert_eq!(output, vec!["true", "true", "true", "false"]);
13791    }
13792
13793    #[test]
13794    fn test_vm_error_type_builtin() {
13795        let output = run_output(
13796            r#"
13797let e1 = DataError::ParseError("bad", "x.csv")
13798let e2 = NetworkError::HttpError("fail", "url")
13799let e3 = "not an error"
13800print(error_type(e1))
13801print(error_type(e2))
13802print(error_type(e3))
13803"#,
13804        );
13805        assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
13806    }
13807
13808    #[test]
13809    fn test_vm_match_error_variants() {
13810        let output = run_output(
13811            r#"
13812fn handle(err) {
13813    print(match err {
13814        DataError::ParseError(msg, _) => "parse: " + msg,
13815        DataError::SchemaError(msg, _, _) => "schema: " + msg,
13816        DataError::ValidationError(_, field) => "validation: " + field,
13817        DataError::NotFound(name) => "not found: " + name,
13818        _ => "unknown"
13819    })
13820}
13821handle(DataError::ParseError("bad csv", "data.csv"))
13822handle(DataError::NotFound("users_table"))
13823handle(DataError::SchemaError("mismatch", "int", "string"))
13824handle(DataError::ValidationError("invalid", "email"))
13825"#,
13826        );
13827        assert_eq!(
13828            output,
13829            vec![
13830                "parse: bad csv",
13831                "not found: users_table",
13832                "schema: mismatch",
13833                "validation: email",
13834            ]
13835        );
13836    }
13837
13838    #[test]
13839    fn test_vm_rethrow_structured_error() {
13840        let output = run_output(
13841            r#"
13842try {
13843    try {
13844        throw DataError::NotFound("config")
13845    } catch e {
13846        throw e
13847    }
13848} catch outer {
13849    match outer {
13850        DataError::NotFound(name) => print("caught: " + name),
13851        _ => print("wrong type"),
13852    }
13853}
13854"#,
13855        );
13856        assert_eq!(output, vec!["caught: config"]);
13857    }
13858
13859    // ── Phase 28: Ownership & Move Semantics ──
13860
13861    #[test]
13862    fn test_vm_pipe_moves_value() {
13863        // x |> f() should consume x — accessing x after pipe gives error
13864        let result = run(r#"
13865fn identity(v) { v }
13866let x = [1, 2, 3]
13867x |> identity()
13868print(x)
13869"#);
13870        assert!(result.is_err());
13871        let err = result.unwrap_err().to_string();
13872        assert!(err.contains("moved"), "Error should mention 'moved': {err}");
13873    }
13874
13875    #[test]
13876    fn test_vm_clone_before_pipe() {
13877        // x.clone() |> f() should not consume x
13878        let output = run_output(
13879            r#"
13880fn identity(v) { v }
13881let x = [1, 2, 3]
13882x.clone() |> identity()
13883print(x)
13884"#,
13885        );
13886        assert_eq!(output, vec!["[1, 2, 3]"]);
13887    }
13888
13889    #[test]
13890    fn test_vm_clone_list_deep() {
13891        // Mutating a cloned list should not affect the original
13892        let output = run_output(
13893            r#"
13894let original = [1, 2, 3]
13895let copy = original.clone()
13896copy[0] = 99
13897print(original)
13898print(copy)
13899"#,
13900        );
13901        assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
13902    }
13903
13904    #[test]
13905    fn test_vm_clone_map() {
13906        let output = run_output(
13907            r#"
13908let m = map_from("a", 1, "b", 2)
13909let m2 = m.clone()
13910m2["a"] = 99
13911print(m)
13912print(m2)
13913"#,
13914        );
13915        assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
13916    }
13917
13918    #[test]
13919    fn test_vm_clone_struct() {
13920        let output = run_output(
13921            r#"
13922struct Point { x: int64, y: int64 }
13923let p = Point { x: 1, y: 2 }
13924let p2 = p.clone()
13925print(p)
13926print(p2)
13927"#,
13928        );
13929        assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
13930    }
13931
13932    #[test]
13933    fn test_vm_ref_read_only() {
13934        // &x should be readable but not mutable
13935        let result = run(r#"
13936let x = [1, 2, 3]
13937let r = &x
13938r[0] = 99
13939"#);
13940        assert!(result.is_err());
13941        let err = result.unwrap_err().to_string();
13942        assert!(
13943            err.contains("Cannot mutate a borrowed reference"),
13944            "Error should mention reference: {err}"
13945        );
13946    }
13947
13948    #[test]
13949    fn test_vm_ref_transparent_read() {
13950        // Reading through a ref should work transparently
13951        let output = run_output(
13952            r#"
13953let x = [1, 2, 3]
13954let r = &x
13955print(len(r))
13956"#,
13957        );
13958        assert_eq!(output, vec!["3"]);
13959    }
13960
13961    #[test]
13962    fn test_vm_parallel_for_basic() {
13963        // parallel for should iterate all elements (runs sequentially in VM)
13964        let output = run_output(
13965            r#"
13966let items = [10, 20, 30]
13967parallel for item in items {
13968    print(item)
13969}
13970"#,
13971        );
13972        assert_eq!(output, vec!["10", "20", "30"]);
13973    }
13974
13975    #[test]
13976    fn test_vm_moved_value_clear_error() {
13977        // Error message should mention .clone()
13978        let result = run(r#"
13979fn f(x) { x }
13980let data = "hello"
13981data |> f()
13982print(data)
13983"#);
13984        assert!(result.is_err());
13985        let err = result.unwrap_err().to_string();
13986        assert!(
13987            err.contains("clone()"),
13988            "Error should suggest .clone(): {err}"
13989        );
13990    }
13991
13992    #[test]
13993    fn test_vm_reassign_after_move() {
13994        // After moving, reassigning the variable should work
13995        let output = run_output(
13996            r#"
13997fn f(x) { x }
13998let x = 1
13999x |> f()
14000let x = 2
14001print(x)
14002"#,
14003        );
14004        assert_eq!(output, vec!["2"]);
14005    }
14006
14007    #[test]
14008    fn test_vm_pipe_chain_move() {
14009        // Chained pipes should work — intermediate values don't need explicit binding
14010        let output = run_output(
14011            r#"
14012fn double(x) { x * 2 }
14013fn add_one(x) { x + 1 }
14014let result = 5 |> double() |> add_one()
14015print(result)
14016"#,
14017        );
14018        assert_eq!(output, vec!["11"]);
14019    }
14020
14021    #[test]
14022    fn test_vm_string_clone() {
14023        // .clone() on string values
14024        let output = run_output(
14025            r#"
14026let s = "hello"
14027let s2 = s.clone()
14028print(s)
14029print(s2)
14030"#,
14031        );
14032        assert_eq!(output, vec!["hello", "hello"]);
14033    }
14034
14035    #[test]
14036    fn test_vm_ref_method_dispatch() {
14037        // Methods should be callable through references
14038        let output = run_output(
14039            r#"
14040let s = "hello world"
14041let r = &s
14042print(r.len())
14043"#,
14044        );
14045        assert_eq!(output, vec!["11"]);
14046    }
14047
14048    #[test]
14049    fn test_vm_ref_member_access() {
14050        // Member access through ref should work
14051        let output = run_output(
14052            r#"
14053struct Point { x: int64, y: int64 }
14054let p = Point { x: 10, y: 20 }
14055let r = &p
14056print(r.x)
14057"#,
14058        );
14059        assert_eq!(output, vec!["10"]);
14060    }
14061
14062    #[test]
14063    fn test_vm_ref_set_member_blocked() {
14064        // Setting a member through a ref should fail
14065        let result = run(r#"
14066struct Point { x: int64, y: int64 }
14067let p = Point { x: 10, y: 20 }
14068let r = &p
14069r.x = 99
14070"#);
14071        assert!(result.is_err());
14072        let err = result.unwrap_err().to_string();
14073        assert!(
14074            err.contains("Cannot mutate a borrowed reference"),
14075            "Error: {err}"
14076        );
14077    }
14078
14079    // ── Phase 29: IR Integration Tests ──
14080
14081    #[test]
14082    fn test_ir_filter_merge_chain() {
14083        // Two adjacent filters should be merged by the IR optimizer
14084        let dir = tempfile::tempdir().unwrap();
14085        let csv = dir.path().join("data.csv");
14086        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14087        let src = format!(
14088            r#"let t = read_csv("{}")
14089let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14090print(r)"#,
14091            csv.to_str().unwrap()
14092        );
14093        let output = run_output(&src);
14094        // Both Alice(30) and Charlie(35) pass both filters
14095        assert!(
14096            output[0].contains("Alice"),
14097            "Output should contain Alice: {}",
14098            output[0]
14099        );
14100        assert!(
14101            output[0].contains("Charlie"),
14102            "Output should contain Charlie: {}",
14103            output[0]
14104        );
14105        assert!(
14106            !output[0].contains("Bob"),
14107            "Output should not contain Bob: {}",
14108            output[0]
14109        );
14110    }
14111
14112    #[test]
14113    fn test_ir_predicate_pushdown_through_select() {
14114        // filter after select should be pushed before select by IR optimizer
14115        let dir = tempfile::tempdir().unwrap();
14116        let csv = dir.path().join("data.csv");
14117        std::fs::write(
14118            &csv,
14119            "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14120        )
14121        .unwrap();
14122        let src = format!(
14123            r#"let t = read_csv("{}")
14124let r = t |> select(name, age) |> filter(age > 25) |> collect()
14125print(r)"#,
14126            csv.to_str().unwrap()
14127        );
14128        let output = run_output(&src);
14129        assert!(output[0].contains("Alice"), "Output should contain Alice");
14130        assert!(
14131            output[0].contains("Charlie"),
14132            "Output should contain Charlie"
14133        );
14134        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14135    }
14136
14137    #[test]
14138    fn test_ir_sort_filter_pushdown() {
14139        // filter after sort should be pushed before sort
14140        let dir = tempfile::tempdir().unwrap();
14141        let csv = dir.path().join("data.csv");
14142        std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14143        let src = format!(
14144            r#"let t = read_csv("{}")
14145let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14146print(r)"#,
14147            csv.to_str().unwrap()
14148        );
14149        let output = run_output(&src);
14150        assert!(output[0].contains("Alice"), "Output should contain Alice");
14151        assert!(
14152            output[0].contains("Charlie"),
14153            "Output should contain Charlie"
14154        );
14155        assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14156    }
14157
14158    #[test]
14159    fn test_ir_multi_operation_chain() {
14160        // Complex chain: filter + select + sort + limit
14161        let dir = tempfile::tempdir().unwrap();
14162        let csv = dir.path().join("data.csv");
14163        std::fs::write(
14164            &csv,
14165            "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14166        )
14167        .unwrap();
14168        let src = format!(
14169            r#"let t = read_csv("{}")
14170let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14171print(r)"#,
14172            csv.to_str().unwrap()
14173        );
14174        let output = run_output(&src);
14175        // Top 2 by age descending among age>22: Charlie(35), Alice(30)
14176        assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14177        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14178    }
14179
14180    #[test]
14181    fn test_ir_pipe_move_semantics_preserved() {
14182        // The source variable should be moved after pipe chain
14183        let dir = tempfile::tempdir().unwrap();
14184        let csv = dir.path().join("data.csv");
14185        std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14186        let src = format!(
14187            r#"let t = read_csv("{}")
14188let r = t |> filter(age > 0) |> collect()
14189print(t)"#,
14190            csv.to_str().unwrap()
14191        );
14192        let result = run(&src);
14193        assert!(result.is_err(), "Should error on use-after-move");
14194    }
14195
14196    #[test]
14197    fn test_ir_non_table_op_fallback() {
14198        // A pipe chain with a non-table op should fall back to legacy path
14199        let output = run_output(
14200            r#"
14201fn double(x) { x * 2 }
14202let result = 5 |> double()
14203print(result)
14204"#,
14205        );
14206        assert_eq!(output, vec!["10"]);
14207    }
14208
14209    #[test]
14210    fn test_ir_mixed_pipe_fallback() {
14211        // A pipe into a builtin (not a table op) should use legacy path
14212        let output = run_output(
14213            r#"
14214let result = [3, 1, 2] |> len()
14215print(result)
14216"#,
14217        );
14218        assert_eq!(output, vec!["3"]);
14219    }
14220
14221    #[test]
14222    fn test_ir_single_filter_roundtrip() {
14223        // Even a single filter goes through IR and round-trips correctly
14224        let dir = tempfile::tempdir().unwrap();
14225        let csv = dir.path().join("data.csv");
14226        std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14227        let src = format!(
14228            r#"let t = read_csv("{}")
14229let r = t |> filter(age > 25) |> collect()
14230print(r)"#,
14231            csv.to_str().unwrap()
14232        );
14233        let output = run_output(&src);
14234        assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14235        assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14236    }
14237
14238    // ── Phase 34: Agent Framework ──
14239
14240    #[test]
14241    fn test_vm_agent_definition() {
14242        let output = run_output(
14243            r#"
14244fn search(query) { "found: " + query }
14245agent bot {
14246    model: "gpt-4o",
14247    system: "You are helpful.",
14248    tools {
14249        search: {
14250            description: "Search the web",
14251            parameters: {}
14252        }
14253    },
14254    max_turns: 5
14255}
14256print(type_of(bot))
14257print(bot)
14258"#,
14259        );
14260        assert_eq!(output, vec!["agent", "<agent bot>"]);
14261    }
14262
14263    #[test]
14264    fn test_vm_agent_minimal() {
14265        let output = run_output(
14266            r#"
14267agent minimal_bot {
14268    model: "claude-sonnet-4-20250514"
14269}
14270print(type_of(minimal_bot))
14271"#,
14272        );
14273        assert_eq!(output, vec!["agent"]);
14274    }
14275
14276    #[test]
14277    fn test_vm_agent_with_base_url() {
14278        let output = run_output(
14279            r#"
14280agent local_bot {
14281    model: "llama3",
14282    base_url: "http://localhost:11434/v1",
14283    max_turns: 3
14284}
14285print(local_bot)
14286"#,
14287        );
14288        assert_eq!(output, vec!["<agent local_bot>"]);
14289    }
14290
14291    #[test]
14292    fn test_vm_agent_multiple_tools() {
14293        let output = run_output(
14294            r#"
14295fn search(query) { "result" }
14296fn weather(city) { "sunny" }
14297agent helper {
14298    model: "gpt-4o",
14299    tools {
14300        search: { description: "Search", parameters: {} },
14301        weather: { description: "Get weather", parameters: {} }
14302    }
14303}
14304print(type_of(helper))
14305"#,
14306        );
14307        assert_eq!(output, vec!["agent"]);
14308    }
14309
14310    #[test]
14311    fn test_vm_agent_lifecycle_hooks_stored() {
14312        let output = run_output(
14313            r#"
14314fn search(q) { "result" }
14315agent bot {
14316    model: "gpt-4o",
14317    tools {
14318        search: { description: "Search", parameters: {} }
14319    },
14320    on_tool_call {
14321        println("tool: " + tool_name)
14322    }
14323    on_complete {
14324        println("done")
14325    }
14326}
14327print(type_of(bot))
14328print(type_of(__agent_bot_on_tool_call__))
14329print(type_of(__agent_bot_on_complete__))
14330"#,
14331        );
14332        assert_eq!(output, vec!["agent", "function", "function"]);
14333    }
14334
14335    #[test]
14336    fn test_vm_agent_lifecycle_hook_callable() {
14337        let output = run_output(
14338            r#"
14339agent bot {
14340    model: "gpt-4o",
14341    on_tool_call {
14342        println("called: " + tool_name + " -> " + tool_result)
14343    }
14344    on_complete {
14345        println("completed")
14346    }
14347}
14348__agent_bot_on_tool_call__("search", "query", "found it")
14349__agent_bot_on_complete__("hello")
14350"#,
14351        );
14352        assert_eq!(output, vec!["called: search -> found it", "completed"]);
14353    }
14354}