1use std::collections::HashMap;
5#[cfg(feature = "native")]
6use std::sync::mpsc;
7use std::sync::{Arc, Mutex, OnceLock};
8
9static 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#[cfg(all(
51 feature = "native",
52 any(feature = "snowflake", feature = "bigquery", feature = "databricks")
53))]
54fn write_args(
55 args: &[VmValue],
56 name: &str,
57) -> Result<(tl_data::DataFrame, String, String, String), TlError> {
58 if args.len() < 3 {
59 return Err(runtime_err(format!(
60 "{name}() expects (table, config, table_name, [mode])"
61 )));
62 }
63 let df = match &args[0] {
64 VmValue::Table(t) => t.df.clone(),
65 _ => return Err(runtime_err(format!("{name}() first arg must be a table"))),
66 };
67 let cfg = match &args[1] {
68 VmValue::String(s) => resolve_tl_config_connection(s),
69 _ => return Err(runtime_err(format!("{name}() config must be a string"))),
70 };
71 let table_name = match &args[2] {
72 VmValue::String(s) => s.to_string(),
73 _ => return Err(runtime_err(format!("{name}() table_name must be a string"))),
74 };
75 let mode = match args.get(3) {
76 None | Some(VmValue::None) => "create".to_string(),
77 Some(VmValue::String(s)) => s.to_string(),
78 _ => return Err(runtime_err(format!("{name}() mode must be a string"))),
79 };
80 Ok((df, cfg, table_name, mode))
81}
82
83fn resolve_tl_config_connection(name: &str) -> String {
87 if name.contains('=') || name.contains("://") {
89 return name.to_string();
90 }
91 let config_path =
93 std::env::var("TL_CONFIG_PATH").unwrap_or_else(|_| "tl_config.json".to_string());
94 let Ok(contents) = std::fs::read_to_string(&config_path) else {
95 return name.to_string();
96 };
97 let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) else {
98 return name.to_string();
99 };
100 if let Some(conn) = json
102 .get("connections")
103 .and_then(|c| c.get(name))
104 .and_then(|v| v.as_str())
105 {
106 return conn.to_string();
107 }
108 if let Some(conn) = json.get(name).and_then(|v| v.as_str()) {
109 return conn.to_string();
110 }
111 name.to_string()
113}
114
115fn vm_values_equal(a: &VmValue, b: &VmValue) -> bool {
117 match (a, b) {
118 (VmValue::Int(x), VmValue::Int(y)) => x == y,
119 (VmValue::Float(x), VmValue::Float(y)) => x == y,
120 (VmValue::String(x), VmValue::String(y)) => x == y,
121 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
122 (VmValue::None, VmValue::None) => true,
123 _ => false,
124 }
125}
126
127#[cfg(feature = "native")]
128fn resolve_package_file(pkg_root: &std::path::Path, remaining: &[&str]) -> Option<String> {
133 if remaining.is_empty() {
134 let src = pkg_root.join("src");
136 for entry in &["lib.tl", "mod.tl", "main.tl"] {
137 let p = src.join(entry);
138 if p.exists() {
139 return Some(p.to_string_lossy().to_string());
140 }
141 }
142 for entry in &["mod.tl", "lib.tl"] {
143 let p = pkg_root.join(entry);
144 if p.exists() {
145 return Some(p.to_string_lossy().to_string());
146 }
147 }
148 return None;
149 }
150
151 let rel = remaining.join("/");
153 let src = pkg_root.join("src");
154
155 let file_path = src.join(format!("{rel}.tl"));
156 if file_path.exists() {
157 return Some(file_path.to_string_lossy().to_string());
158 }
159
160 let dir_path = src.join(&rel).join("mod.tl");
161 if dir_path.exists() {
162 return Some(dir_path.to_string_lossy().to_string());
163 }
164
165 let file_path = pkg_root.join(format!("{rel}.tl"));
167 if file_path.exists() {
168 return Some(file_path.to_string_lossy().to_string());
169 }
170
171 let dir_path = pkg_root.join(&rel).join("mod.tl");
172 if dir_path.exists() {
173 return Some(dir_path.to_string_lossy().to_string());
174 }
175
176 if remaining.len() > 1 {
178 let parent = &remaining[..remaining.len() - 1];
179 let parent_rel = parent.join("/");
180 let parent_file = src.join(format!("{parent_rel}.tl"));
181 if parent_file.exists() {
182 return Some(parent_file.to_string_lossy().to_string());
183 }
184 let parent_file = pkg_root.join(format!("{parent_rel}.tl"));
185 if parent_file.exists() {
186 return Some(parent_file.to_string_lossy().to_string());
187 }
188 }
189
190 None
191}
192
193fn vm_json_to_value(v: &serde_json::Value) -> VmValue {
195 match v {
196 serde_json::Value::Null => VmValue::None,
197 serde_json::Value::Bool(b) => VmValue::Bool(*b),
198 serde_json::Value::Number(n) => {
199 if let Some(i) = n.as_i64() {
200 VmValue::Int(i)
201 } else {
202 VmValue::Float(n.as_f64().unwrap_or(0.0))
203 }
204 }
205 serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
206 serde_json::Value::Array(arr) => {
207 VmValue::List(Box::new(arr.iter().map(vm_json_to_value).collect()))
208 }
209 serde_json::Value::Object(obj) => VmValue::Map(Box::new(
210 obj.iter()
211 .map(|(k, v)| (Arc::from(k.as_str()), vm_json_to_value(v)))
212 .collect(),
213 )),
214 }
215}
216
217fn vm_value_to_json(v: &VmValue) -> serde_json::Value {
219 match v {
220 VmValue::None => serde_json::Value::Null,
221 VmValue::Bool(b) => serde_json::Value::Bool(*b),
222 VmValue::Int(n) => serde_json::json!(*n),
223 VmValue::Float(n) => serde_json::json!(*n),
224 VmValue::String(s) => serde_json::Value::String(s.to_string()),
225 VmValue::List(items) => {
226 serde_json::Value::Array(items.iter().map(vm_value_to_json).collect())
227 }
228 VmValue::Map(pairs) => {
229 let obj: serde_json::Map<String, serde_json::Value> = pairs
230 .iter()
231 .map(|(k, v)| (k.to_string(), vm_value_to_json(v)))
232 .collect();
233 serde_json::Value::Object(obj)
234 }
235 VmValue::Secret(_) => serde_json::Value::String("***".to_string()),
236 _ => serde_json::Value::String(format!("{v}")),
237 }
238}
239
240#[cfg(feature = "native")]
242const PARALLEL_THRESHOLD: usize = 10_000;
243
244#[cfg(feature = "native")]
246fn is_pure_closure(func: &VmValue) -> bool {
247 match func {
248 VmValue::Function(closure) => closure.upvalues.is_empty(),
249 _ => false,
250 }
251}
252
253#[cfg(feature = "native")]
256fn execute_pure_fn(proto: &Arc<Prototype>, args: &[VmValue]) -> Result<VmValue, TlError> {
257 let base = 0;
258 let num_regs = proto.num_registers as usize;
259 let mut stack = vec![VmValue::None; num_regs + 1];
260 for (i, arg) in args.iter().enumerate() {
261 stack[i] = arg.clone();
262 }
263
264 let mut ip = 0;
265 loop {
266 if ip >= proto.code.len() {
267 return Ok(VmValue::None);
268 }
269 let inst = proto.code[ip];
270 let op = decode_op(inst);
271 let a = decode_a(inst);
272 let b = decode_b(inst);
273 let c = decode_c(inst);
274 let bx = decode_bx(inst);
275 let sbx = decode_sbx(inst);
276
277 ip += 1;
278
279 match op {
280 Op::LoadConst => {
281 let val = match &proto.constants[bx as usize] {
282 Constant::Int(n) => VmValue::Int(*n),
283 Constant::Float(n) => VmValue::Float(*n),
284 Constant::String(s) => VmValue::String(s.clone()),
285 Constant::Decimal(s) => {
286 use std::str::FromStr;
287 VmValue::Decimal(rust_decimal::Decimal::from_str(s).unwrap_or_default())
288 }
289 _ => VmValue::None,
290 };
291 stack[base + a as usize] = val;
292 }
293 Op::LoadNone => stack[base + a as usize] = VmValue::None,
294 Op::LoadTrue => stack[base + a as usize] = VmValue::Bool(true),
295 Op::LoadFalse => stack[base + a as usize] = VmValue::Bool(false),
296 Op::Move | Op::GetLocal => {
297 let val = stack[base + b as usize].clone();
298 stack[base + a as usize] = val;
299 }
300 Op::SetLocal => {
301 let val = stack[base + a as usize].clone();
302 stack[base + b as usize] = val;
303 }
304 Op::Add => {
305 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
306 (VmValue::Int(x), VmValue::Int(y)) => x
307 .checked_add(*y)
308 .map(VmValue::Int)
309 .unwrap_or_else(|| VmValue::Float(*x as f64 + *y as f64)),
310 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x + y),
311 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 + y),
312 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x + *y as f64),
313 _ => return Err(runtime_err("Cannot add in parallel fn")),
314 };
315 stack[base + a as usize] = result;
316 }
317 Op::Sub => {
318 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
319 (VmValue::Int(x), VmValue::Int(y)) => x
320 .checked_sub(*y)
321 .map(VmValue::Int)
322 .unwrap_or_else(|| VmValue::Float(*x as f64 - *y as f64)),
323 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x - y),
324 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 - y),
325 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x - *y as f64),
326 _ => return Err(runtime_err("Cannot subtract in parallel fn")),
327 };
328 stack[base + a as usize] = result;
329 }
330 Op::Mul => {
331 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
332 (VmValue::Int(x), VmValue::Int(y)) => x
333 .checked_mul(*y)
334 .map(VmValue::Int)
335 .unwrap_or_else(|| VmValue::Float(*x as f64 * *y as f64)),
336 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x * y),
337 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 * y),
338 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x * *y as f64),
339 _ => return Err(runtime_err("Cannot multiply in parallel fn")),
340 };
341 stack[base + a as usize] = result;
342 }
343 Op::Div => {
344 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
345 (VmValue::Int(x), VmValue::Int(y)) => {
346 if *y == 0 {
347 return Err(runtime_err("Division by zero"));
348 }
349 VmValue::Int(x / y)
350 }
351 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x / y),
352 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float(*x as f64 / y),
353 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x / *y as f64),
354 _ => return Err(runtime_err("Cannot divide in parallel fn")),
355 };
356 stack[base + a as usize] = result;
357 }
358 Op::Mod => {
359 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
360 (VmValue::Int(x), VmValue::Int(y)) => {
361 if *y == 0 {
362 return Err(runtime_err("Modulo by zero"));
363 }
364 VmValue::Int(x % y)
365 }
366 (VmValue::Float(x), VmValue::Float(y)) => {
367 if *y == 0.0 {
368 return Err(runtime_err("Modulo by zero"));
369 }
370 VmValue::Float(x % y)
371 }
372 _ => return Err(runtime_err("Cannot modulo in parallel fn")),
373 };
374 stack[base + a as usize] = result;
375 }
376 Op::Pow => {
377 let result = match (&stack[base + b as usize], &stack[base + c as usize]) {
378 (VmValue::Int(x), VmValue::Int(y)) => {
379 VmValue::Int((*x as f64).powi(*y as i32) as i64)
380 }
381 (VmValue::Float(x), VmValue::Float(y)) => VmValue::Float(x.powf(*y)),
382 (VmValue::Int(x), VmValue::Float(y)) => VmValue::Float((*x as f64).powf(*y)),
383 (VmValue::Float(x), VmValue::Int(y)) => VmValue::Float(x.powi(*y as i32)),
384 _ => return Err(runtime_err("Cannot pow in parallel fn")),
385 };
386 stack[base + a as usize] = result;
387 }
388 Op::Neg => {
389 let result = match &stack[base + b as usize] {
390 VmValue::Int(n) => VmValue::Int(-n),
391 VmValue::Float(n) => VmValue::Float(-n),
392 _ => return Err(runtime_err("Cannot negate in parallel fn")),
393 };
394 stack[base + a as usize] = result;
395 }
396 Op::Eq => {
397 let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
398 (VmValue::Int(x), VmValue::Int(y)) => x == y,
399 (VmValue::Float(x), VmValue::Float(y)) => x == y,
400 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
401 (VmValue::String(x), VmValue::String(y)) => x == y,
402 (VmValue::None, VmValue::None) => true,
403 _ => false,
404 };
405 stack[base + a as usize] = VmValue::Bool(eq);
406 }
407 Op::Neq => {
408 let eq = match (&stack[base + b as usize], &stack[base + c as usize]) {
409 (VmValue::Int(x), VmValue::Int(y)) => x == y,
410 (VmValue::Float(x), VmValue::Float(y)) => x == y,
411 (VmValue::Bool(x), VmValue::Bool(y)) => x == y,
412 (VmValue::String(x), VmValue::String(y)) => x == y,
413 (VmValue::None, VmValue::None) => true,
414 _ => false,
415 };
416 stack[base + a as usize] = VmValue::Bool(!eq);
417 }
418 Op::Lt | Op::Gt | Op::Lte | Op::Gte => {
419 let cmp = match (&stack[base + b as usize], &stack[base + c as usize]) {
420 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y) as i8,
421 (VmValue::Float(x), VmValue::Float(y)) => {
422 if x < y {
423 -1
424 } else if x > y {
425 1
426 } else {
427 0
428 }
429 }
430 _ => return Err(runtime_err("Cannot compare in parallel fn")),
431 };
432 let result = match op {
433 Op::Lt => cmp < 0,
434 Op::Gt => cmp > 0,
435 Op::Lte => cmp <= 0,
436 Op::Gte => cmp >= 0,
437 _ => unreachable!(),
438 };
439 stack[base + a as usize] = VmValue::Bool(result);
440 }
441 Op::And => {
442 let left = stack[base + b as usize].is_truthy();
443 let right = stack[base + c as usize].is_truthy();
444 stack[base + a as usize] = VmValue::Bool(left && right);
445 }
446 Op::Or => {
447 let left = stack[base + b as usize].is_truthy();
448 let right = stack[base + c as usize].is_truthy();
449 stack[base + a as usize] = VmValue::Bool(left || right);
450 }
451 Op::Not => {
452 let val = !stack[base + b as usize].is_truthy();
453 stack[base + a as usize] = VmValue::Bool(val);
454 }
455 Op::Jump => {
456 ip = (ip as i32 + sbx as i32) as usize;
457 }
458 Op::JumpIfFalse => {
459 if !stack[base + a as usize].is_truthy() {
460 ip = (ip as i32 + sbx as i32) as usize;
461 }
462 }
463 Op::JumpIfTrue => {
464 if stack[base + a as usize].is_truthy() {
465 ip = (ip as i32 + sbx as i32) as usize;
466 }
467 }
468 Op::Return => {
469 return Ok(stack[base + a as usize].clone());
470 }
471 _ => return Err(runtime_err("Unsupported op in parallel function")),
473 }
474 }
475}
476
477struct CallFrame {
479 prototype: Arc<Prototype>,
480 ip: usize,
481 base: usize,
482 upvalues: Vec<UpvalueRef>,
483}
484
485struct TryHandler {
487 frame_idx: usize,
489 catch_ip: usize,
491}
492
493pub struct Vm {
495 pub stack: Vec<VmValue>,
497 frames: Vec<CallFrame>,
499 pub globals: HashMap<String, VmValue>,
501 #[cfg(feature = "native")]
503 data_engine: Option<DataEngine>,
504 pub output: Vec<String>,
506 try_handlers: Vec<TryHandler>,
508 yielded_value: Option<VmValue>,
510 yielded_ip: usize,
512 pub file_path: Option<String>,
514 module_cache: HashMap<String, HashMap<String, VmValue>>,
516 importing_files: std::collections::HashSet<String>,
518 pub public_items: std::collections::HashSet<String>,
520 pub package_roots: HashMap<String, std::path::PathBuf>,
522 pub project_root: Option<std::path::PathBuf>,
524 pub schema_registry: crate::schema::SchemaRegistry,
526 pub secret_vault: SecretVault,
528 pub security_policy: Option<crate::security::SecurityPolicy>,
530 #[cfg(feature = "async-runtime")]
532 runtime: Option<Arc<tokio::runtime::Runtime>>,
533 thrown_value: Option<VmValue>,
535 #[cfg(feature = "gpu")]
537 gpu_ops: Option<tl_gpu::GpuOps>,
538 #[cfg(feature = "mcp")]
540 mcp_agent_clients: HashMap<String, Vec<Arc<tl_mcp::McpClient>>>,
541}
542
543#[derive(Debug, Clone, Default)]
545pub struct SecretVault(HashMap<String, String>);
546
547impl SecretVault {
548 pub fn new() -> Self {
549 Self(HashMap::new())
550 }
551 pub fn get(&self, key: &str) -> Option<&String> {
552 self.0.get(key)
553 }
554 pub fn insert(&mut self, key: String, val: String) {
555 self.0.insert(key, val);
556 }
557 pub fn remove(&mut self, key: &str) {
558 self.0.remove(key);
559 }
560 pub fn keys(&self) -> impl Iterator<Item = &String> {
561 self.0.keys()
562 }
563}
564
565impl Drop for SecretVault {
566 fn drop(&mut self) {
567 for val in self.0.values_mut() {
568 unsafe {
571 let ptr = val.as_mut_vec().as_mut_ptr();
572 std::ptr::write_bytes(ptr, 0, val.len());
573 }
574 }
575 self.0.clear();
576 }
577}
578
579impl Vm {
580 pub fn new() -> Self {
581 let mut vm = Vm {
582 stack: Vec::with_capacity(256),
583 frames: Vec::new(),
584 globals: HashMap::new(),
585 #[cfg(feature = "native")]
586 data_engine: None,
587 output: Vec::new(),
588 try_handlers: Vec::new(),
589 yielded_value: None,
590 yielded_ip: 0,
591 file_path: None,
592 module_cache: HashMap::new(),
593 importing_files: std::collections::HashSet::new(),
594 public_items: std::collections::HashSet::new(),
595 package_roots: HashMap::new(),
596 project_root: None,
597 schema_registry: crate::schema::SchemaRegistry::new(),
598 secret_vault: SecretVault::new(),
599 security_policy: None,
600 #[cfg(feature = "async-runtime")]
601 runtime: None,
602 thrown_value: None,
603 #[cfg(feature = "gpu")]
604 gpu_ops: None,
605 #[cfg(feature = "mcp")]
606 mcp_agent_clients: HashMap::new(),
607 };
608 vm.globals.insert(
610 "DataError".into(),
611 VmValue::EnumDef(Arc::new(VmEnumDef {
612 name: Arc::from("DataError"),
613 variants: vec![
614 (Arc::from("ParseError"), 2),
615 (Arc::from("SchemaError"), 3),
616 (Arc::from("ValidationError"), 2),
617 (Arc::from("NotFound"), 1),
618 ],
619 })),
620 );
621 vm.globals.insert(
622 "NetworkError".into(),
623 VmValue::EnumDef(Arc::new(VmEnumDef {
624 name: Arc::from("NetworkError"),
625 variants: vec![
626 (Arc::from("ConnectionError"), 2),
627 (Arc::from("TimeoutError"), 1),
628 (Arc::from("HttpError"), 2),
629 ],
630 })),
631 );
632 vm.globals.insert(
633 "ConnectorError".into(),
634 VmValue::EnumDef(Arc::new(VmEnumDef {
635 name: Arc::from("ConnectorError"),
636 variants: vec![
637 (Arc::from("AuthError"), 2),
638 (Arc::from("QueryError"), 2),
639 (Arc::from("ConfigError"), 2),
640 ],
641 })),
642 );
643 #[cfg(feature = "mcp")]
645 {
646 vm.globals.insert(
647 "mcp_connect".to_string(),
648 VmValue::Builtin(BuiltinId::McpConnect),
649 );
650 vm.globals.insert(
651 "mcp_list_tools".to_string(),
652 VmValue::Builtin(BuiltinId::McpListTools),
653 );
654 vm.globals.insert(
655 "mcp_call_tool".to_string(),
656 VmValue::Builtin(BuiltinId::McpCallTool),
657 );
658 vm.globals.insert(
659 "mcp_disconnect".to_string(),
660 VmValue::Builtin(BuiltinId::McpDisconnect),
661 );
662 vm.globals.insert(
663 "mcp_serve".to_string(),
664 VmValue::Builtin(BuiltinId::McpServe),
665 );
666 vm.globals.insert(
667 "mcp_server_info".to_string(),
668 VmValue::Builtin(BuiltinId::McpServerInfo),
669 );
670 vm.globals
671 .insert("mcp_ping".to_string(), VmValue::Builtin(BuiltinId::McpPing));
672 vm.globals.insert(
673 "mcp_list_resources".to_string(),
674 VmValue::Builtin(BuiltinId::McpListResources),
675 );
676 vm.globals.insert(
677 "mcp_read_resource".to_string(),
678 VmValue::Builtin(BuiltinId::McpReadResource),
679 );
680 vm.globals.insert(
681 "mcp_list_prompts".to_string(),
682 VmValue::Builtin(BuiltinId::McpListPrompts),
683 );
684 vm.globals.insert(
685 "mcp_get_prompt".to_string(),
686 VmValue::Builtin(BuiltinId::McpGetPrompt),
687 );
688 }
689 vm
690 }
691
692 #[cfg(feature = "async-runtime")]
694 fn ensure_runtime(&mut self) -> Arc<tokio::runtime::Runtime> {
695 if self.runtime.is_none() {
696 self.runtime = Some(Arc::new(
697 tokio::runtime::Builder::new_multi_thread()
698 .enable_all()
699 .build()
700 .expect("Failed to create tokio runtime"),
701 ));
702 }
703 self.runtime.as_ref().unwrap().clone()
704 }
705
706 #[cfg(feature = "gpu")]
708 fn get_gpu_ops(&mut self) -> Result<&tl_gpu::GpuOps, TlError> {
709 if self.gpu_ops.is_none() {
710 let device =
711 tl_gpu::GpuDevice::get().ok_or_else(|| runtime_err("No GPU device available"))?;
712 self.gpu_ops = Some(tl_gpu::GpuOps::new(device));
713 }
714 Ok(self.gpu_ops.as_ref().unwrap())
715 }
716
717 #[cfg(feature = "gpu")]
719 fn ensure_gpu_tensor(&mut self, val: &VmValue) -> Result<Arc<tl_gpu::GpuTensor>, TlError> {
720 match val {
721 VmValue::GpuTensor(gt) => Ok(gt.clone()),
722 #[cfg(feature = "native")]
723 VmValue::Tensor(t) => {
724 let device = tl_gpu::GpuDevice::get()
725 .ok_or_else(|| runtime_err("No GPU device available"))?;
726 Ok(Arc::new(tl_gpu::GpuTensor::from_cpu(t, device)))
727 }
728 _ => Err(runtime_err(format!(
729 "Expected tensor or gpu_tensor, got {}",
730 val.type_name()
731 ))),
732 }
733 }
734
735 #[cfg(feature = "native")]
736 fn engine(&mut self) -> &DataEngine {
737 if self.data_engine.is_none() {
738 self.data_engine = Some(DataEngine::new());
739 }
740 self.data_engine.as_ref().unwrap()
741 }
742
743 fn ensure_stack(&mut self, size: usize) {
745 if self.stack.len() < size {
746 self.stack.resize(size, VmValue::None);
747 }
748 }
749
750 pub fn execute(&mut self, proto: &Prototype) -> Result<VmValue, TlError> {
752 let proto = Arc::new(proto.clone());
753 let base = self.stack.len();
754 self.ensure_stack(base + proto.num_registers as usize + 1);
755
756 self.frames.push(CallFrame {
757 prototype: proto,
758 ip: 0,
759 base,
760 upvalues: Vec::new(),
761 });
762
763 self.run().map_err(|e| self.enrich_error(e))
764 }
765
766 pub fn debug_load(&mut self, proto: &Prototype) {
770 let proto = Arc::new(proto.clone());
771 let base = self.stack.len();
772 self.ensure_stack(base + proto.num_registers as usize + 1);
773 self.frames.push(CallFrame {
774 prototype: proto,
775 ip: 0,
776 base,
777 upvalues: Vec::new(),
778 });
779 }
780
781 pub fn debug_step(&mut self) -> Result<Option<VmValue>, TlError> {
786 let entry_depth = 1; self.run_step(entry_depth).map_err(|e| self.enrich_error(e))
788 }
789
790 pub fn debug_current_line(&self) -> u32 {
792 if let Some(frame) = self.frames.last() {
793 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
794 if ip < frame.prototype.lines.len() {
795 frame.prototype.lines[ip]
796 } else {
797 0
798 }
799 } else {
800 0
801 }
802 }
803
804 pub fn debug_current_function(&self) -> String {
806 self.frames
807 .last()
808 .map(|f| f.prototype.name.clone())
809 .unwrap_or_default()
810 }
811
812 pub fn debug_is_done(&self) -> bool {
814 self.frames.is_empty()
815 || self
816 .frames
817 .last()
818 .is_some_and(|f| f.ip >= f.prototype.code.len())
819 }
820
821 pub fn debug_get_global(&self, name: &str) -> Option<&VmValue> {
823 self.globals.get(name)
824 }
825
826 pub fn debug_get_local(&self, name: &str) -> Option<&VmValue> {
828 if let Some(frame) = self.frames.last() {
829 for (local_name, reg) in &frame.prototype.top_level_locals {
830 if local_name == name {
831 let idx = frame.base + *reg as usize;
832 if idx < self.stack.len() {
833 return Some(&self.stack[idx]);
834 }
835 }
836 }
837 }
838 None
839 }
840
841 pub fn debug_locals(&self) -> Vec<(String, &VmValue)> {
843 let mut result = Vec::new();
844 if let Some(frame) = self.frames.last() {
845 for (name, reg) in &frame.prototype.top_level_locals {
846 let idx = frame.base + *reg as usize;
847 if idx < self.stack.len() {
848 result.push((name.clone(), &self.stack[idx]));
849 }
850 }
851 }
852 result
853 }
854
855 pub fn debug_current_ip(&self) -> usize {
857 self.frames.last().map(|f| f.ip).unwrap_or(0)
858 }
859
860 pub fn debug_step_line(&mut self) -> Result<Option<VmValue>, TlError> {
862 let start_line = self.debug_current_line();
863 loop {
864 if self.debug_is_done() {
865 return Ok(Some(VmValue::None));
866 }
867 let result = self.debug_step()?;
868 if result.is_some() {
869 return Ok(result);
870 }
871 let new_line = self.debug_current_line();
872 if new_line != start_line && new_line != 0 {
873 return Ok(None);
874 }
875 }
876 }
877
878 pub fn debug_continue(&mut self, breakpoints: &[u32]) -> Result<Option<VmValue>, TlError> {
880 loop {
881 if self.debug_is_done() {
882 return Ok(Some(VmValue::None));
883 }
884 let result = self.debug_step()?;
885 if result.is_some() {
886 return Ok(result);
887 }
888 let line = self.debug_current_line();
889 if breakpoints.contains(&line) {
890 return Ok(None);
891 }
892 }
893 }
894
895 fn enrich_error(&self, err: TlError) -> TlError {
897 match err {
898 TlError::Runtime(mut re) => {
899 let mut trace = Vec::new();
901 for frame in self.frames.iter().rev() {
902 let ip = if frame.ip > 0 { frame.ip - 1 } else { 0 };
903 let line = if ip < frame.prototype.lines.len() {
904 frame.prototype.lines[ip]
905 } else {
906 0
907 };
908 trace.push(tl_errors::StackFrame {
909 function: frame.prototype.name.clone(),
910 line,
911 });
912 }
913 if re.span.is_none() && !trace.is_empty() && trace[0].line > 0 {
915 }
919 re.stack_trace = trace;
920 TlError::Runtime(re)
921 }
922 other => other,
923 }
924 }
925
926 fn run(&mut self) -> Result<VmValue, TlError> {
928 let entry_depth = self.frames.len();
929 loop {
930 let step_result = self.run_step(entry_depth);
931 match step_result {
932 Ok(Some(val)) => return Ok(val), Ok(None) => continue, Err(e) => {
935 if let Some(handler) = self.try_handlers.pop() {
937 while self.frames.len() > handler.frame_idx {
939 self.frames.pop();
940 }
941 if self.frames.is_empty() {
942 return Err(e);
943 }
944 let fidx = self.frames.len() - 1;
945 self.frames[fidx].ip = handler.catch_ip;
946 let err_msg = match &e {
947 TlError::Runtime(re) => re.message.clone(),
948 other => format!("{other}"),
949 };
950 let catch_val = self
954 .thrown_value
955 .take()
956 .unwrap_or_else(|| VmValue::String(Arc::from(err_msg.as_str())));
957 let cbase = self.frames[fidx].base;
958 let current_ip = self.frames[fidx].ip;
959 if current_ip < self.frames[fidx].prototype.code.len() {
960 let catch_inst = self.frames[fidx].prototype.code[current_ip];
961 let catch_op = decode_op(catch_inst);
962 let catch_reg = decode_a(catch_inst);
963 if matches!(catch_op, Op::LoadNone) {
964 self.frames[fidx].ip += 1;
966 self.ensure_stack(cbase + catch_reg as usize + 1);
967 self.stack[cbase + catch_reg as usize] = catch_val;
968 }
969 }
970 continue;
971 }
972 return Err(e);
973 }
974 }
975 }
976 }
977
978 fn run_step(&mut self, entry_depth: usize) -> Result<Option<VmValue>, TlError> {
980 if self.frames.len() < entry_depth || self.frames.is_empty() {
981 return Ok(Some(VmValue::None));
982 }
983 let frame_idx = self.frames.len() - 1;
984 let frame = &self.frames[frame_idx];
985
986 if frame.ip >= frame.prototype.code.len() {
987 self.frames.pop();
989 return Ok(Some(VmValue::None));
990 }
991
992 let inst = frame.prototype.code[frame.ip];
993 let op = decode_op(inst);
994 let a = decode_a(inst);
995 let b = decode_b(inst);
996 let c = decode_c(inst);
997 let bx = decode_bx(inst);
998 let sbx = decode_sbx(inst);
999 let base = frame.base;
1000
1001 self.frames[frame_idx].ip += 1;
1003
1004 match op {
1005 Op::LoadConst => {
1006 let val = self.load_constant(frame_idx, bx)?;
1007 self.stack[base + a as usize] = val;
1008 }
1009 Op::LoadNone => {
1010 self.stack[base + a as usize] = VmValue::None;
1011 }
1012 Op::LoadTrue => {
1013 self.stack[base + a as usize] = VmValue::Bool(true);
1014 }
1015 Op::LoadFalse => {
1016 self.stack[base + a as usize] = VmValue::Bool(false);
1017 }
1018 Op::Move => {
1019 let val = &self.stack[base + b as usize];
1020 if matches!(val, VmValue::Moved) {
1021 return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
1022 }
1023 self.stack[base + a as usize] = val.clone();
1024 }
1025 Op::GetLocal => {
1026 let val = &self.stack[base + b as usize];
1027 if matches!(val, VmValue::Moved) {
1028 return Err(runtime_err("Use of moved value. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy.".to_string()));
1029 }
1030 self.stack[base + a as usize] = val.clone();
1031 }
1032 Op::SetLocal => {
1033 let val = self.stack[base + a as usize].clone();
1034 self.stack[base + b as usize] = val;
1035 }
1036 Op::GetGlobal => {
1037 let name = self.get_string_constant(frame_idx, bx)?;
1038 let val = self
1039 .globals
1040 .get(name.as_ref())
1041 .cloned()
1042 .unwrap_or(VmValue::None);
1043 if matches!(val, VmValue::Moved) {
1044 return Err(runtime_err(format!(
1045 "Use of moved value `{name}`. It was consumed by a pipe (|>) operation. Use .clone() to keep a copy."
1046 )));
1047 }
1048 self.stack[base + a as usize] = val;
1049 }
1050 Op::SetGlobal => {
1051 let name = self.get_string_constant(frame_idx, bx)?;
1052 let val = self.stack[base + a as usize].clone();
1053 #[cfg(feature = "native")]
1055 if let VmValue::String(ref s) = val {
1056 if s.starts_with("__schema__:") {
1057 self.process_schema_global(s);
1058 } else if s.starts_with("__migrate__:") {
1059 self.process_migrate_global(s);
1060 }
1061 }
1062 self.globals.insert(name.to_string(), val);
1063 }
1064 Op::GetUpvalue => {
1065 let val = {
1066 let frame = &self.frames[frame_idx];
1067 match &frame.upvalues[b as usize] {
1068 UpvalueRef::Open { stack_index } => self.stack[*stack_index].clone(),
1069 UpvalueRef::Closed(v) => v.clone(),
1070 }
1071 };
1072 self.stack[base + a as usize] = val;
1073 }
1074 Op::SetUpvalue => {
1075 let val = self.stack[base + a as usize].clone();
1076 let frame = &mut self.frames[frame_idx];
1077 match &mut frame.upvalues[b as usize] {
1078 UpvalueRef::Open { stack_index } => {
1079 let idx = *stack_index;
1080 self.stack[idx] = val;
1081 }
1082 UpvalueRef::Closed(v) => {
1083 *v = val;
1084 }
1085 }
1086 }
1087 Op::Add => {
1088 let result = self.vm_add(base, b, c)?;
1089 self.stack[base + a as usize] = result;
1090 }
1091 Op::Sub => {
1092 let result = self.vm_sub(base, b, c)?;
1093 self.stack[base + a as usize] = result;
1094 }
1095 Op::Mul => {
1096 let result = self.vm_mul(base, b, c)?;
1097 self.stack[base + a as usize] = result;
1098 }
1099 Op::Div => {
1100 let result = self.vm_div(base, b, c)?;
1101 self.stack[base + a as usize] = result;
1102 }
1103 Op::Mod => {
1104 let result = self.vm_mod(base, b, c)?;
1105 self.stack[base + a as usize] = result;
1106 }
1107 Op::Pow => {
1108 let result = self.vm_pow(base, b, c)?;
1109 self.stack[base + a as usize] = result;
1110 }
1111 Op::Neg => {
1112 let result = match &self.stack[base + b as usize] {
1113 VmValue::Int(n) => VmValue::Int(-n),
1114 VmValue::Float(n) => VmValue::Float(-n),
1115 VmValue::Decimal(d) => VmValue::Decimal(-d),
1116 other => {
1117 return Err(runtime_err(format!("Cannot negate {}", other.type_name())));
1118 }
1119 };
1120 self.stack[base + a as usize] = result;
1121 }
1122 Op::Eq => {
1123 let result = self.vm_eq(base, b, c);
1124 self.stack[base + a as usize] = VmValue::Bool(result);
1125 }
1126 Op::Neq => {
1127 let result = !self.vm_eq(base, b, c);
1128 self.stack[base + a as usize] = VmValue::Bool(result);
1129 }
1130 Op::Lt => {
1131 let result = self.vm_cmp(base, b, c)?;
1132 self.stack[base + a as usize] = VmValue::Bool(result == Some(-1));
1133 }
1134 Op::Gt => {
1135 let result = self.vm_cmp(base, b, c)?;
1136 self.stack[base + a as usize] = VmValue::Bool(result == Some(1));
1137 }
1138 Op::Lte => {
1139 let result = self.vm_cmp(base, b, c)?;
1140 self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(-1) | Some(0)));
1141 }
1142 Op::Gte => {
1143 let result = self.vm_cmp(base, b, c)?;
1144 self.stack[base + a as usize] = VmValue::Bool(matches!(result, Some(0) | Some(1)));
1145 }
1146 Op::And => {
1147 let left = self.stack[base + b as usize].is_truthy();
1148 let right = self.stack[base + c as usize].is_truthy();
1149 self.stack[base + a as usize] = VmValue::Bool(left && right);
1150 }
1151 Op::Or => {
1152 let left = self.stack[base + b as usize].is_truthy();
1153 let right = self.stack[base + c as usize].is_truthy();
1154 self.stack[base + a as usize] = VmValue::Bool(left || right);
1155 }
1156 Op::Not => {
1157 let val = !self.stack[base + b as usize].is_truthy();
1158 self.stack[base + a as usize] = VmValue::Bool(val);
1159 }
1160 Op::Concat => {
1161 let left = format!("{}", self.stack[base + b as usize]);
1162 let right = format!("{}", self.stack[base + c as usize]);
1163 self.stack[base + a as usize] =
1164 VmValue::String(Arc::from(format!("{left}{right}").as_str()));
1165 }
1166 Op::Jump => {
1167 let frame = &mut self.frames[frame_idx];
1168 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1169 }
1170 Op::JumpIfFalse => {
1171 if !self.stack[base + a as usize].is_truthy() {
1172 let frame = &mut self.frames[frame_idx];
1173 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1174 }
1175 }
1176 Op::JumpIfTrue => {
1177 if self.stack[base + a as usize].is_truthy() {
1178 let frame = &mut self.frames[frame_idx];
1179 frame.ip = (frame.ip as i32 + sbx as i32) as usize;
1180 }
1181 }
1182 Op::Call => {
1183 let func_val = self.stack[base + a as usize].clone();
1185 self.do_call(func_val, base, a, b, c)?;
1186 }
1187 Op::Return => {
1188 let return_val = self.stack[base + a as usize].clone();
1189 self.frames.pop();
1190 return Ok(Some(return_val));
1191 }
1192 Op::Closure => {
1193 let proto = match &self.frames[frame_idx].prototype.constants[bx as usize] {
1194 Constant::Prototype(p) => p.clone(),
1195 _ => return Err(runtime_err("Expected prototype constant")),
1196 };
1197
1198 let mut upvalues = Vec::new();
1200 for def in &proto.upvalue_defs {
1201 if def.is_local {
1202 upvalues.push(UpvalueRef::Open {
1203 stack_index: base + def.index as usize,
1204 });
1205 } else {
1206 let frame = &self.frames[frame_idx];
1207 upvalues.push(frame.upvalues[def.index as usize].clone());
1208 }
1209 }
1210
1211 let closure = VmClosure {
1212 prototype: proto,
1213 upvalues,
1214 };
1215 self.stack[base + a as usize] = VmValue::Function(Arc::new(closure));
1216 }
1217 Op::NewList => {
1218 let mut items = Vec::with_capacity(c as usize);
1220 for i in 0..c as usize {
1221 items.push(self.stack[base + b as usize + i].clone());
1222 }
1223 self.stack[base + a as usize] = VmValue::List(Box::new(items));
1224 }
1225 Op::GetIndex => {
1226 let raw_obj = &self.stack[base + b as usize];
1227 let obj = match raw_obj {
1228 VmValue::Ref(inner) => inner.as_ref(),
1229 other => other,
1230 };
1231 let idx = &self.stack[base + c as usize];
1232 let result = match (obj, idx) {
1233 (VmValue::List(items), VmValue::Int(i)) => {
1234 let idx = if *i < 0 {
1235 let adjusted = items.len() as i64 + *i;
1236 if adjusted < 0 {
1237 return Err(runtime_err(format!(
1238 "Index {} out of bounds for list of length {}",
1239 i,
1240 items.len()
1241 )));
1242 }
1243 adjusted as usize
1244 } else {
1245 *i as usize
1246 };
1247 items.get(idx).cloned().ok_or_else(|| {
1248 runtime_err(format!(
1249 "Index {} out of bounds for list of length {}",
1250 i,
1251 items.len()
1252 ))
1253 })?
1254 }
1255 (VmValue::Map(pairs), VmValue::String(key)) => pairs
1256 .iter()
1257 .find(|(k, _)| k.as_ref() == key.as_ref())
1258 .map(|(_, v)| v.clone())
1259 .unwrap_or(VmValue::None),
1260 _ => {
1261 return Err(runtime_err(format!(
1262 "Cannot index {} with {}",
1263 obj.type_name(),
1264 idx.type_name()
1265 )));
1266 }
1267 };
1268 self.stack[base + a as usize] = result;
1269 }
1270 Op::SetIndex => {
1271 if matches!(&self.stack[base + b as usize], VmValue::Ref(_)) {
1272 return Err(runtime_err(
1273 "Cannot mutate a borrowed reference".to_string(),
1274 ));
1275 }
1276 let val = self.stack[base + a as usize].clone();
1277 let idx_val = self.stack[base + c as usize].clone();
1278 match idx_val {
1279 VmValue::Int(i) => {
1280 if let VmValue::List(ref mut items) = self.stack[base + b as usize] {
1281 let idx = if i < 0 {
1282 let adjusted = items.len() as i64 + i;
1283 if adjusted < 0 {
1284 return Err(runtime_err(format!(
1285 "Index {} out of bounds for list of length {}",
1286 i,
1287 items.len()
1288 )));
1289 }
1290 adjusted as usize
1291 } else {
1292 i as usize
1293 };
1294 if idx < items.len() {
1295 items[idx] = val;
1296 } else {
1297 return Err(runtime_err(format!(
1298 "Index {} out of bounds for list of length {}",
1299 i,
1300 items.len()
1301 )));
1302 }
1303 }
1304 }
1305 VmValue::String(key) => {
1306 if let VmValue::Map(ref mut pairs) = self.stack[base + b as usize] {
1307 if let Some(entry) =
1308 pairs.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref())
1309 {
1310 entry.1 = val;
1311 } else {
1312 pairs.push((key, val));
1313 }
1314 }
1315 }
1316 _ => {}
1317 }
1318 }
1319 Op::NewMap => {
1320 let mut pairs = Vec::with_capacity(c as usize);
1323 for i in 0..c as usize {
1324 let key_val = &self.stack[base + b as usize + i * 2];
1325 let val = self.stack[base + b as usize + i * 2 + 1].clone();
1326 let key = match key_val {
1327 VmValue::String(s) => s.clone(),
1328 other => Arc::from(format!("{other}").as_str()),
1329 };
1330 pairs.push((key, val));
1331 }
1332 self.stack[base + a as usize] = VmValue::Map(Box::new(pairs));
1333 }
1334 Op::TablePipe => {
1335 #[cfg(feature = "native")]
1336 {
1337 let table_val = self.stack[base + a as usize].clone();
1339 let result = self.handle_table_pipe(frame_idx, table_val, b, c)?;
1340 self.stack[base + a as usize] = result;
1341 }
1342 #[cfg(not(feature = "native"))]
1343 {
1344 let _ = (a, b, c, frame_idx);
1345 return Err(runtime_err("Table operations not available in WASM"));
1346 }
1347 }
1348 Op::CallBuiltin => {
1349 let builtin_id = decode_bx(inst);
1352 let next_inst = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1353 self.frames[frame_idx].ip += 1;
1354 let arg_count = decode_a(next_inst) as usize;
1355 let first_arg = decode_b(next_inst) as usize;
1356
1357 let result = self.call_builtin(builtin_id, base + first_arg, arg_count)?;
1358 self.stack[base + a as usize] = result;
1359 }
1360 Op::ForIter => {
1361 let idx = match &self.stack[base + a as usize] {
1363 VmValue::Int(i) => *i as usize,
1364 _ => 0,
1365 };
1366 let list = &self.stack[base + b as usize];
1367 let done = match list {
1368 VmValue::List(items) if idx < items.len() => {
1369 let item = items[idx].clone();
1370 self.stack[base + c as usize] = item;
1371 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1372 false
1373 }
1374 VmValue::Map(pairs) if idx < pairs.len() => {
1375 let (k, v) = &pairs[idx];
1376 let pair =
1377 VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]));
1378 self.stack[base + c as usize] = pair;
1379 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1380 false
1381 }
1382 VmValue::Set(items) if idx < items.len() => {
1383 let item = items[idx].clone();
1384 self.stack[base + c as usize] = item;
1385 self.stack[base + a as usize] = VmValue::Int((idx + 1) as i64);
1386 false
1387 }
1388 VmValue::Generator(gen_arc) => {
1389 let g = gen_arc.clone();
1390 let val = self.generator_next(&g)?;
1391 if matches!(val, VmValue::None) {
1392 true
1393 } else {
1394 self.stack[base + c as usize] = val;
1395 false
1396 }
1397 }
1398 _ => true,
1399 };
1400 if done {
1401 } else {
1404 self.frames[frame_idx].ip += 1;
1406 }
1407 }
1408 Op::ForPrep => {
1409 }
1411 Op::TestMatch => {
1412 let subject = &self.stack[base + a as usize];
1414 let pattern = &self.stack[base + b as usize];
1415 let matched = match (subject, pattern) {
1416 (VmValue::Int(a), VmValue::Int(b)) => a == b,
1417 (VmValue::Float(a), VmValue::Float(b)) => a == b,
1418 (VmValue::String(a), VmValue::String(b)) => a == b,
1419 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
1420 (VmValue::None, VmValue::None) => true,
1421 (VmValue::EnumInstance(subj), VmValue::EnumInstance(pat)) => {
1423 subj.type_name == pat.type_name && subj.variant == pat.variant
1424 }
1425 (VmValue::StructInstance(s), VmValue::String(name)) => {
1427 s.type_name.as_ref() == name.as_ref()
1428 }
1429 _ => false,
1430 };
1431 self.stack[base + c as usize] = VmValue::Bool(matched);
1432 }
1433 Op::NullCoalesce => {
1434 if matches!(self.stack[base + a as usize], VmValue::None) {
1435 let val = self.stack[base + b as usize].clone();
1436 self.stack[base + a as usize] = val;
1437 }
1438 }
1439 Op::GetMember => {
1440 let field_name = self.get_string_constant(frame_idx, c as u16)?;
1442 let raw_obj = self.stack[base + b as usize].clone();
1443 let obj = match &raw_obj {
1444 VmValue::Ref(inner) => inner.as_ref().clone(),
1445 _ => raw_obj,
1446 };
1447 let result = match &obj {
1448 VmValue::StructInstance(inst) => inst
1449 .fields
1450 .iter()
1451 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1452 .map(|(_, v)| v.clone())
1453 .unwrap_or(VmValue::None),
1454 VmValue::Module(m) => m
1455 .exports
1456 .get(field_name.as_ref())
1457 .cloned()
1458 .unwrap_or(VmValue::None),
1459 VmValue::EnumInstance(e) => match field_name.as_ref() {
1460 "variant" => VmValue::String(e.variant.clone()),
1461 "type_name" => VmValue::String(e.type_name.clone()),
1462 _ => VmValue::None,
1463 },
1464 VmValue::Map(pairs) => pairs
1465 .iter()
1466 .find(|(k, _)| k.as_ref() == field_name.as_ref())
1467 .map(|(_, v)| v.clone())
1468 .unwrap_or(VmValue::None),
1469 #[cfg(feature = "python")]
1470 VmValue::PyObject(wrapper) => {
1471 crate::python::py_get_member(wrapper, field_name.as_ref())
1472 }
1473 _ => VmValue::None,
1474 };
1475 self.stack[base + a as usize] = result;
1476 }
1477 Op::Interpolate => {
1478 let template = self.get_string_constant(frame_idx, bx)?;
1480 let result = self.interpolate_string(&template, base)?;
1481 self.stack[base + a as usize] = VmValue::String(Arc::from(result.as_str()));
1482 }
1483 Op::Train => {
1484 #[cfg(feature = "native")]
1485 {
1486 let result = self.handle_train(frame_idx, b, c)?;
1487 self.stack[base + a as usize] = result;
1488 }
1489 #[cfg(not(feature = "native"))]
1490 {
1491 let _ = (a, b, c, frame_idx);
1492 return Err(runtime_err("AI training not available in WASM"));
1493 }
1494 }
1495 Op::PipelineExec => {
1496 #[cfg(feature = "native")]
1497 {
1498 let result = self.handle_pipeline_exec(frame_idx, b, c)?;
1499 self.stack[base + a as usize] = result;
1500 }
1501 #[cfg(not(feature = "native"))]
1502 {
1503 let _ = (a, b, c, frame_idx);
1504 return Err(runtime_err("Pipelines not available in WASM"));
1505 }
1506 }
1507 Op::StreamExec => {
1508 #[cfg(feature = "native")]
1509 {
1510 let result = self.handle_stream_exec(frame_idx, b)?;
1511 self.stack[base + a as usize] = result;
1512 }
1513 #[cfg(not(feature = "native"))]
1514 {
1515 let _ = (a, b, frame_idx);
1516 return Err(runtime_err("Streaming not available in WASM"));
1517 }
1518 }
1519 Op::ConnectorDecl => {
1520 #[cfg(feature = "native")]
1521 {
1522 let result = self.handle_connector_decl(frame_idx, b, c)?;
1523 self.stack[base + a as usize] = result;
1524 }
1525 #[cfg(not(feature = "native"))]
1526 {
1527 let _ = (a, b, c, frame_idx);
1528 return Err(runtime_err("Connectors not available in WASM"));
1529 }
1530 }
1531
1532 Op::NewStruct => {
1534 let name = self.get_string_constant(frame_idx, b as u16)?;
1541
1542 let is_decl = (c & 0x80) != 0;
1546
1547 if is_decl {
1548 let const_idx = (c & 0x7F) as usize;
1549 let fields_data = match &self.frames[frame_idx].prototype.constants[const_idx] {
1551 Constant::AstExprList(exprs) => exprs.clone(),
1552 _ => Vec::new(),
1553 };
1554 let is_enum = fields_data
1556 .first()
1557 .map(|e| {
1558 if let AstExpr::String(s) = e {
1559 s.contains(':')
1560 } else {
1561 false
1562 }
1563 })
1564 .unwrap_or(false);
1565
1566 if is_enum {
1567 let variants: Vec<(Arc<str>, usize)> = fields_data
1568 .iter()
1569 .filter_map(|e| {
1570 if let AstExpr::String(s) = e {
1571 let parts: Vec<&str> = s.splitn(2, ':').collect();
1572 if parts.len() == 2 {
1573 Some((
1574 Arc::from(parts[0]),
1575 parts[1].parse::<usize>().unwrap_or(0),
1576 ))
1577 } else {
1578 None
1579 }
1580 } else {
1581 None
1582 }
1583 })
1584 .collect();
1585 self.stack[base + a as usize] = VmValue::EnumDef(Arc::new(VmEnumDef {
1586 name: name.clone(),
1587 variants,
1588 }));
1589 } else {
1590 let field_names: Vec<Arc<str>> = fields_data
1591 .iter()
1592 .filter_map(|e| {
1593 if let AstExpr::String(s) = e {
1594 Some(Arc::from(s.as_str()))
1595 } else {
1596 None
1597 }
1598 })
1599 .collect();
1600 self.stack[base + a as usize] = VmValue::StructDef(Arc::new(VmStructDef {
1601 name: name.clone(),
1602 fields: field_names,
1603 }));
1604 }
1605 } else {
1606 let field_count = c as usize;
1608 let next_ip = self.frames[frame_idx].ip;
1610 let next = self.frames[frame_idx]
1611 .prototype
1612 .code
1613 .get(next_ip)
1614 .copied()
1615 .unwrap_or(0);
1616 let start_reg = decode_a(next) as usize;
1617 self.frames[frame_idx].ip += 1; let mut fields = Vec::new();
1620 for i in 0..field_count {
1621 let fname = self.stack[base + start_reg + i * 2].clone();
1622 let fval = self.stack[base + start_reg + i * 2 + 1].clone();
1623 let fname_str = match fname {
1624 VmValue::String(s) => s,
1625 _ => Arc::from(format!("field_{i}").as_str()),
1626 };
1627 fields.push((fname_str, fval));
1628 }
1629 self.stack[base + a as usize] =
1630 VmValue::StructInstance(Arc::new(VmStructInstance {
1631 type_name: name.clone(),
1632 fields,
1633 }));
1634 }
1635 }
1636
1637 Op::SetMember => {
1638 if matches!(&self.stack[base + a as usize], VmValue::Ref(_)) {
1639 return Err(runtime_err(
1640 "Cannot mutate a borrowed reference".to_string(),
1641 ));
1642 }
1643 let field_name = self.get_string_constant(frame_idx, b as u16)?;
1645 let val = self.stack[base + c as usize].clone();
1646 let obj = self.stack[base + a as usize].clone();
1647 if let VmValue::StructInstance(inst) = obj {
1648 let mut new_fields = inst.fields.clone();
1649 let mut found = false;
1650 for (k, v) in &mut new_fields {
1651 if k.as_ref() == field_name.as_ref() {
1652 *v = val.clone();
1653 found = true;
1654 break;
1655 }
1656 }
1657 if !found {
1658 new_fields.push((field_name, val));
1659 }
1660 self.stack[base + a as usize] =
1661 VmValue::StructInstance(Arc::new(VmStructInstance {
1662 type_name: inst.type_name.clone(),
1663 fields: new_fields,
1664 }));
1665 }
1666 }
1667
1668 Op::NewEnum => {
1669 let full_name = self.get_string_constant(frame_idx, b as u16)?;
1672 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1673 self.frames[frame_idx].ip += 1;
1674 let arg_count = decode_a(next) as usize;
1675 let args_start = c as usize;
1676
1677 let parts: Vec<&str> = full_name.splitn(2, "::").collect();
1679 let (type_name, variant) = if parts.len() == 2 {
1680 (Arc::from(parts[0]), Arc::from(parts[1]))
1681 } else {
1682 (Arc::from(""), Arc::from(full_name.as_ref()))
1683 };
1684
1685 let mut fields = Vec::new();
1686 for i in 0..arg_count {
1687 fields.push(self.stack[base + args_start + i].clone());
1688 }
1689
1690 self.stack[base + a as usize] = VmValue::EnumInstance(Arc::new(VmEnumInstance {
1691 type_name,
1692 variant,
1693 fields,
1694 }));
1695 }
1696
1697 Op::MatchEnum => {
1698 let variant_name = self.get_string_constant(frame_idx, b as u16)?;
1700 let subject = &self.stack[base + a as usize];
1701 let matched = match subject {
1702 VmValue::EnumInstance(e) => e.variant.as_ref() == variant_name.as_ref(),
1703 _ => false,
1704 };
1705 self.stack[base + c as usize] = VmValue::Bool(matched);
1706 }
1707
1708 Op::MethodCall => {
1709 let method_name = self.get_string_constant(frame_idx, c as u16)?;
1712 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1713 self.frames[frame_idx].ip += 1;
1714 let args_start = decode_a(next) as usize;
1715 let arg_count = decode_b(next) as usize;
1716
1717 let obj = self.stack[base + b as usize].clone();
1718 let mut args = Vec::new();
1719 for i in 0..arg_count {
1720 args.push(self.stack[base + args_start + i].clone());
1721 }
1722
1723 let result = self.dispatch_method(obj, &method_name, &args)?;
1724 self.stack[base + a as usize] = result;
1725 }
1726
1727 Op::Throw => {
1728 let val = self.stack[base + a as usize].clone();
1730 self.thrown_value = Some(val.clone());
1731 let err_msg = format!("{val}");
1732 return Err(runtime_err(err_msg));
1733 }
1734
1735 Op::TryBegin => {
1736 let catch_ip = (self.frames[frame_idx].ip as i32 + sbx as i32) as usize;
1738 self.try_handlers.push(TryHandler {
1739 frame_idx: self.frames.len(),
1740 catch_ip,
1741 });
1742 }
1743
1744 Op::TryEnd => {
1745 self.try_handlers.pop();
1747 }
1748
1749 Op::Import => {
1750 #[cfg(feature = "native")]
1751 {
1752 let path = self.get_string_constant(frame_idx, bx)?;
1757 let next = self.frames[frame_idx].prototype.code[self.frames[frame_idx].ip];
1758 self.frames[frame_idx].ip += 1;
1759 let next_a = decode_a(next);
1760 let next_b = decode_b(next);
1761 let next_c = decode_c(next);
1762
1763 let result = if next_c == 0xAB {
1764 self.handle_use_import(&path, next_a, next_b, frame_idx)?
1766 } else {
1767 let alias_idx = next_a as u16;
1769 let alias = self.get_string_constant(frame_idx, alias_idx)?;
1770 self.handle_import(&path, &alias)?
1771 };
1772 self.stack[base + a as usize] = result;
1773 }
1774 #[cfg(not(feature = "native"))]
1775 {
1776 let _ = (a, bx, frame_idx);
1777 return Err(runtime_err("Module imports not available in WASM"));
1778 }
1779 }
1780
1781 Op::Await => {
1782 let val = self.stack[base + b as usize].clone();
1784 match val {
1785 VmValue::Task(task) => {
1786 let rx = {
1787 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
1788 guard.take()
1789 };
1790 match rx {
1791 Some(receiver) => match receiver.recv() {
1792 Ok(Ok(result)) => {
1793 self.stack[base + a as usize] = result;
1794 }
1795 Ok(Err(err_msg)) => {
1796 return Err(runtime_err(err_msg));
1797 }
1798 Err(_) => {
1799 return Err(runtime_err("Task channel disconnected"));
1800 }
1801 },
1802 None => {
1803 return Err(runtime_err("Task already awaited"));
1804 }
1805 }
1806 }
1807 other => {
1809 self.stack[base + a as usize] = other;
1810 }
1811 }
1812 }
1813 Op::Yield => {
1814 let val = self.stack[base + a as usize].clone();
1816 self.yielded_value = Some(val.clone());
1817 self.yielded_ip = self.frames[frame_idx].ip;
1819 self.frames.pop();
1821 return Ok(Some(val));
1822 }
1823 Op::TryPropagate => {
1824 let src = self.stack[base + b as usize].clone();
1830 match &src {
1831 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
1832 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
1833 self.stack[base + a as usize] = ei.fields[0].clone();
1835 } else if ei.variant.as_ref() == "Err" {
1836 self.frames.pop();
1838 return Ok(Some(src));
1839 } else {
1840 self.stack[base + a as usize] = src;
1841 }
1842 }
1843 VmValue::None => {
1844 self.frames.pop();
1846 return Ok(Some(VmValue::None));
1847 }
1848 _ => {
1849 self.stack[base + a as usize] = src;
1851 }
1852 }
1853 }
1854 Op::ExtractField => {
1855 let source = self.stack[base + b as usize].clone();
1858 let is_rest = (c & 0x80) != 0;
1859 let idx = (c & 0x7F) as usize;
1860 let val = if is_rest {
1861 match &source {
1863 VmValue::List(l) => {
1864 if idx < l.len() {
1865 VmValue::List(Box::new(l[idx..].to_vec()))
1866 } else {
1867 VmValue::List(Box::default())
1868 }
1869 }
1870 _ => VmValue::List(Box::default()),
1871 }
1872 } else {
1873 match &source {
1874 VmValue::EnumInstance(ei) => {
1875 ei.fields.get(idx).cloned().unwrap_or(VmValue::None)
1876 }
1877 VmValue::List(l) => l.get(idx).cloned().unwrap_or(VmValue::None),
1878 _ => VmValue::None,
1879 }
1880 };
1881 self.stack[base + a as usize] = val;
1882 }
1883 Op::ExtractNamedField => {
1884 let source = self.stack[base + b as usize].clone();
1886 let field_name = match &self.frames[frame_idx].prototype.constants[c as usize] {
1887 Constant::String(s) => s.clone(),
1888 _ => return Err(runtime_err("ExtractNamedField: expected string constant")),
1889 };
1890 let val = match &source {
1891 VmValue::StructInstance(s) => s
1892 .fields
1893 .iter()
1894 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1895 .map(|(_, v)| v.clone())
1896 .unwrap_or(VmValue::None),
1897 VmValue::Map(m) => m
1898 .iter()
1899 .find(|(k, _): &&(Arc<str>, VmValue)| k.as_ref() == field_name.as_ref())
1900 .map(|(_, v)| v.clone())
1901 .unwrap_or(VmValue::None),
1902 _ => VmValue::None,
1903 };
1904 self.stack[base + a as usize] = val;
1905 }
1906
1907 Op::LoadMoved => {
1909 self.stack[base + a as usize] = VmValue::Moved;
1910 }
1911 Op::MakeRef => {
1912 let val = self.stack[base + b as usize].clone();
1913 self.stack[base + a as usize] = VmValue::Ref(Arc::new(val));
1914 }
1915 Op::ParallelFor => {
1916 }
1919 Op::AgentExec => {
1920 #[cfg(feature = "native")]
1921 {
1922 let result = self.handle_agent_exec(frame_idx, b, c)?;
1923 self.stack[base + a as usize] = result;
1924 }
1925 #[cfg(not(feature = "native"))]
1926 {
1927 let _ = (a, b, c, frame_idx);
1928 return Err(runtime_err("Agents not available in WASM".to_string()));
1929 }
1930 }
1931 }
1932 Ok(None)
1933 }
1934
1935 fn do_call(
1937 &mut self,
1938 func: VmValue,
1939 caller_base: usize,
1940 func_reg: u8,
1941 args_start: u8,
1942 arg_count: u8,
1943 ) -> Result<(), TlError> {
1944 const MAX_CALL_DEPTH: usize = 512;
1945 if self.frames.len() >= MAX_CALL_DEPTH {
1946 return Err(runtime_err(
1947 "Stack overflow: maximum recursion depth (512) exceeded",
1948 ));
1949 }
1950 match func {
1951 VmValue::Function(closure) => {
1952 let proto = closure.prototype.clone();
1953 let arity = proto.arity as usize;
1954
1955 if arg_count as usize != arity {
1956 return Err(runtime_err(format!(
1957 "Expected {} arguments, got {}",
1958 arity, arg_count
1959 )));
1960 }
1961
1962 if proto.is_generator {
1964 let mut closed_upvalues = Vec::new();
1966 for uv in &closure.upvalues {
1967 match uv {
1968 UpvalueRef::Open { stack_index } => {
1969 let val = self.stack[*stack_index].clone();
1970 closed_upvalues.push(UpvalueRef::Closed(val));
1971 }
1972 UpvalueRef::Closed(v) => {
1973 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
1974 }
1975 }
1976 }
1977
1978 let num_regs = proto.num_registers as usize;
1980 let mut saved_stack = vec![VmValue::None; num_regs];
1981 for (i, slot) in saved_stack.iter_mut().enumerate().take(arg_count as usize) {
1982 *slot = self.stack[caller_base + args_start as usize + i].clone();
1983 }
1984
1985 let gn = VmGenerator::new(GeneratorKind::UserDefined {
1986 prototype: proto,
1987 upvalues: closed_upvalues,
1988 saved_stack,
1989 ip: 0,
1990 });
1991 self.stack[caller_base + func_reg as usize] =
1992 VmValue::Generator(Arc::new(Mutex::new(gn)));
1993 return Ok(());
1994 }
1995
1996 let new_base = self.stack.len();
1998 self.ensure_stack(new_base + proto.num_registers as usize + 1);
1999
2000 for i in 0..arg_count as usize {
2002 self.stack[new_base + i] =
2003 self.stack[caller_base + args_start as usize + i].clone();
2004 }
2005
2006 self.frames.push(CallFrame {
2007 prototype: proto,
2008 ip: 0,
2009 base: new_base,
2010 upvalues: closure.upvalues.clone(),
2011 });
2012
2013 let result = self.run()?;
2015
2016 let result = self.close_upvalues_in_value(result, new_base);
2018
2019 self.stack[caller_base + func_reg as usize] = result;
2021
2022 self.stack.truncate(new_base);
2024
2025 Ok(())
2026 }
2027 VmValue::Builtin(builtin_id) => {
2028 let result = self.call_builtin(
2029 builtin_id as u16,
2030 caller_base + args_start as usize,
2031 arg_count as usize,
2032 )?;
2033 self.stack[caller_base + func_reg as usize] = result;
2034 Ok(())
2035 }
2036 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
2037 }
2038 }
2039
2040 fn value_may_need_closing(val: &VmValue) -> bool {
2045 match val {
2046 VmValue::Function(_) => true,
2047 VmValue::List(items) => items.iter().any(Self::value_may_need_closing),
2048 VmValue::Map(entries) => entries.iter().any(|(_, v)| Self::value_may_need_closing(v)),
2049 _ => false,
2050 }
2051 }
2052
2053 fn close_upvalues_in_value(&self, val: VmValue, frame_base: usize) -> VmValue {
2054 match val {
2055 VmValue::Function(ref closure) => {
2056 let needs_closing = closure.upvalues.iter().any(|uv| {
2058 matches!(uv, UpvalueRef::Open { stack_index } if *stack_index >= frame_base)
2059 });
2060 if !needs_closing {
2061 return val;
2062 }
2063 let closed_upvalues: Vec<UpvalueRef> = closure
2064 .upvalues
2065 .iter()
2066 .map(|uv| match uv {
2067 UpvalueRef::Open { stack_index } if *stack_index >= frame_base => {
2068 UpvalueRef::Closed(self.stack[*stack_index].clone())
2069 }
2070 other => other.clone(),
2071 })
2072 .collect();
2073 VmValue::Function(Arc::new(VmClosure {
2074 prototype: closure.prototype.clone(),
2075 upvalues: closed_upvalues,
2076 }))
2077 }
2078 VmValue::List(items) => {
2079 if !items.iter().any(Self::value_may_need_closing) {
2080 return VmValue::List(items);
2081 }
2082 VmValue::List(Box::new(
2083 (*items)
2084 .into_iter()
2085 .map(|v| self.close_upvalues_in_value(v, frame_base))
2086 .collect(),
2087 ))
2088 }
2089 VmValue::Map(entries) => {
2090 if !entries.iter().any(|(_, v)| Self::value_may_need_closing(v)) {
2091 return VmValue::Map(entries);
2092 }
2093 VmValue::Map(Box::new(
2094 (*entries)
2095 .into_iter()
2096 .map(|(k, v)| (k, self.close_upvalues_in_value(v, frame_base)))
2097 .collect(),
2098 ))
2099 }
2100 other => other,
2101 }
2102 }
2103
2104 pub(crate) fn execute_closure(
2106 &mut self,
2107 proto: &Arc<Prototype>,
2108 upvalues: &[UpvalueRef],
2109 ) -> Result<VmValue, TlError> {
2110 let base = self.stack.len();
2111 self.ensure_stack(base + proto.num_registers as usize + 1);
2112 self.frames.push(CallFrame {
2113 prototype: proto.clone(),
2114 ip: 0,
2115 base,
2116 upvalues: upvalues.to_vec(),
2117 });
2118 self.run()
2119 }
2120
2121 pub(crate) fn execute_closure_with_args(
2123 &mut self,
2124 proto: &Arc<Prototype>,
2125 upvalues: &[UpvalueRef],
2126 args: &[VmValue],
2127 ) -> Result<VmValue, TlError> {
2128 let base = self.stack.len();
2129 self.ensure_stack(base + proto.num_registers as usize + 1);
2130 for (i, arg) in args.iter().enumerate() {
2131 self.stack[base + i] = arg.clone();
2132 }
2133 self.frames.push(CallFrame {
2134 prototype: proto.clone(),
2135 ip: 0,
2136 base,
2137 upvalues: upvalues.to_vec(),
2138 });
2139 self.run()
2140 }
2141
2142 fn load_constant(&self, frame_idx: usize, idx: u16) -> Result<VmValue, TlError> {
2143 let frame = &self.frames[frame_idx];
2144 match &frame.prototype.constants[idx as usize] {
2145 Constant::Int(n) => Ok(VmValue::Int(*n)),
2146 Constant::Float(f) => Ok(VmValue::Float(*f)),
2147 Constant::String(s) => Ok(VmValue::String(s.clone())),
2148 Constant::Prototype(p) => {
2149 Ok(VmValue::Function(Arc::new(VmClosure {
2151 prototype: p.clone(),
2152 upvalues: Vec::new(),
2153 })))
2154 }
2155 Constant::Decimal(s) => {
2156 use std::str::FromStr;
2157 Ok(VmValue::Decimal(
2158 rust_decimal::Decimal::from_str(s).unwrap_or_default(),
2159 ))
2160 }
2161 Constant::AstExpr(_) | Constant::AstExprList(_) => Ok(VmValue::None),
2162 }
2163 }
2164
2165 fn get_string_constant(&self, frame_idx: usize, idx: u16) -> Result<Arc<str>, TlError> {
2166 let frame = &self.frames[frame_idx];
2167 match &frame.prototype.constants[idx as usize] {
2168 Constant::String(s) => Ok(s.clone()),
2169 _ => Err(runtime_err("Expected string constant")),
2170 }
2171 }
2172
2173 fn vm_add(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2176 let left = &self.stack[base + b as usize];
2177 let right = &self.stack[base + c as usize];
2178 match (left, right) {
2179 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2180 .checked_add(*b)
2181 .map(VmValue::Int)
2182 .unwrap_or_else(|| VmValue::Float(*a as f64 + *b as f64))),
2183 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a + b)),
2184 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 + b)),
2185 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a + *b as f64)),
2186 (VmValue::String(a), VmValue::String(b)) => {
2187 Ok(VmValue::String(Arc::from(format!("{a}{b}").as_str())))
2188 }
2189 #[cfg(feature = "gpu")]
2190 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2191 let a = a.clone();
2192 let b = b.clone();
2193 let ops = self.get_gpu_ops()?;
2194 let result = ops.add(&a, &b).map_err(runtime_err)?;
2195 Ok(VmValue::GpuTensor(Arc::new(result)))
2196 }
2197 #[cfg(feature = "gpu")]
2198 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2199 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2200 let lv = self.stack[base + b as usize].clone();
2201 let rv = self.stack[base + c as usize].clone();
2202 let a = self.ensure_gpu_tensor(&lv)?;
2203 let b_val = self.ensure_gpu_tensor(&rv)?;
2204 let ops = self.get_gpu_ops()?;
2205 let result = ops.add(&a, &b_val).map_err(runtime_err)?;
2206 Ok(VmValue::GpuTensor(Arc::new(result)))
2207 }
2208 #[cfg(feature = "native")]
2209 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2210 let result = a.add(b).map_err(|e| runtime_err(e.to_string()))?;
2211 Ok(VmValue::Tensor(Arc::new(result)))
2212 }
2213 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a + b)),
2215 (VmValue::Decimal(a), VmValue::Int(b)) => {
2216 Ok(VmValue::Decimal(a + rust_decimal::Decimal::from(*b)))
2217 }
2218 (VmValue::Int(a), VmValue::Decimal(b)) => {
2219 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) + b))
2220 }
2221 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) + b)),
2222 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a + decimal_to_f64(b))),
2223 _ => Err(runtime_err(format!(
2224 "Cannot apply `+` to {} and {}",
2225 left.type_name(),
2226 right.type_name()
2227 ))),
2228 }
2229 }
2230
2231 fn vm_sub(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2232 let left = &self.stack[base + b as usize];
2233 let right = &self.stack[base + c as usize];
2234 match (left, right) {
2235 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2236 .checked_sub(*b)
2237 .map(VmValue::Int)
2238 .unwrap_or_else(|| VmValue::Float(*a as f64 - *b as f64))),
2239 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a - b)),
2240 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 - b)),
2241 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a - *b as f64)),
2242 #[cfg(feature = "gpu")]
2243 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2244 let a = a.clone();
2245 let b = b.clone();
2246 let ops = self.get_gpu_ops()?;
2247 let result = ops.sub(&a, &b).map_err(runtime_err)?;
2248 Ok(VmValue::GpuTensor(Arc::new(result)))
2249 }
2250 #[cfg(feature = "gpu")]
2251 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2252 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2253 let lv = self.stack[base + b as usize].clone();
2254 let rv = self.stack[base + c as usize].clone();
2255 let a = self.ensure_gpu_tensor(&lv)?;
2256 let b_val = self.ensure_gpu_tensor(&rv)?;
2257 let ops = self.get_gpu_ops()?;
2258 let result = ops.sub(&a, &b_val).map_err(runtime_err)?;
2259 Ok(VmValue::GpuTensor(Arc::new(result)))
2260 }
2261 #[cfg(feature = "native")]
2262 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2263 let result = a.sub(b).map_err(|e| runtime_err(e.to_string()))?;
2264 Ok(VmValue::Tensor(Arc::new(result)))
2265 }
2266 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a - b)),
2267 (VmValue::Decimal(a), VmValue::Int(b)) => {
2268 Ok(VmValue::Decimal(a - rust_decimal::Decimal::from(*b)))
2269 }
2270 (VmValue::Int(a), VmValue::Decimal(b)) => {
2271 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) - b))
2272 }
2273 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) - b)),
2274 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a - decimal_to_f64(b))),
2275 _ => Err(runtime_err(format!(
2276 "Cannot apply `-` to {} and {}",
2277 left.type_name(),
2278 right.type_name()
2279 ))),
2280 }
2281 }
2282
2283 fn vm_mul(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2284 let left = &self.stack[base + b as usize];
2285 let right = &self.stack[base + c as usize];
2286 match (left, right) {
2287 (VmValue::Int(a), VmValue::Int(b)) => Ok(a
2288 .checked_mul(*b)
2289 .map(VmValue::Int)
2290 .unwrap_or_else(|| VmValue::Float(*a as f64 * *b as f64))),
2291 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a * b)),
2292 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float(*a as f64 * b)),
2293 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a * *b as f64)),
2294 (VmValue::String(a), VmValue::Int(b)) => {
2295 if *b < 0 {
2296 return Err(runtime_err(
2297 "Cannot repeat string a negative number of times",
2298 ));
2299 }
2300 if *b > 10_000_000 {
2301 return Err(runtime_err(
2302 "String repeat count too large (max 10,000,000)",
2303 ));
2304 }
2305 Ok(VmValue::String(Arc::from(a.repeat(*b as usize).as_str())))
2306 }
2307 #[cfg(feature = "gpu")]
2308 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2309 let a = a.clone();
2310 let b = b.clone();
2311 let ops = self.get_gpu_ops()?;
2312 let result = ops.mul(&a, &b).map_err(runtime_err)?;
2313 Ok(VmValue::GpuTensor(Arc::new(result)))
2314 }
2315 #[cfg(feature = "gpu")]
2316 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2317 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2318 let lv = self.stack[base + b as usize].clone();
2319 let rv = self.stack[base + c as usize].clone();
2320 let a = self.ensure_gpu_tensor(&lv)?;
2321 let b_val = self.ensure_gpu_tensor(&rv)?;
2322 let ops = self.get_gpu_ops()?;
2323 let result = ops.mul(&a, &b_val).map_err(runtime_err)?;
2324 Ok(VmValue::GpuTensor(Arc::new(result)))
2325 }
2326 #[cfg(feature = "gpu")]
2327 (VmValue::GpuTensor(t), VmValue::Float(s))
2328 | (VmValue::Float(s), VmValue::GpuTensor(t)) => {
2329 let t = t.clone();
2330 let s = *s;
2331 let ops = self.get_gpu_ops()?;
2332 let result = ops.scale(&t, s as f32);
2333 Ok(VmValue::GpuTensor(Arc::new(result)))
2334 }
2335 #[cfg(feature = "native")]
2336 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2337 let result = a.mul(b).map_err(|e| runtime_err(e.to_string()))?;
2338 Ok(VmValue::Tensor(Arc::new(result)))
2339 }
2340 #[cfg(feature = "native")]
2341 (VmValue::Tensor(t), VmValue::Float(s)) | (VmValue::Float(s), VmValue::Tensor(t)) => {
2342 let result = t.scale(*s);
2343 Ok(VmValue::Tensor(Arc::new(result)))
2344 }
2345 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(VmValue::Decimal(a * b)),
2346 (VmValue::Decimal(a), VmValue::Int(b)) => {
2347 Ok(VmValue::Decimal(a * rust_decimal::Decimal::from(*b)))
2348 }
2349 (VmValue::Int(a), VmValue::Decimal(b)) => {
2350 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) * b))
2351 }
2352 (VmValue::Decimal(a), VmValue::Float(b)) => Ok(VmValue::Float(decimal_to_f64(a) * b)),
2353 (VmValue::Float(a), VmValue::Decimal(b)) => Ok(VmValue::Float(a * decimal_to_f64(b))),
2354 _ => Err(runtime_err(format!(
2355 "Cannot apply `*` to {} and {}",
2356 left.type_name(),
2357 right.type_name()
2358 ))),
2359 }
2360 }
2361
2362 fn vm_div(&mut self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2363 let left = &self.stack[base + b as usize];
2364 let right = &self.stack[base + c as usize];
2365 match (left, right) {
2366 (VmValue::Int(a), VmValue::Int(b)) => {
2367 if *b == 0 {
2368 return Err(runtime_err("Division by zero"));
2369 }
2370 Ok(VmValue::Int(a / b))
2371 }
2372 (VmValue::Float(a), VmValue::Float(b)) => {
2373 if *b == 0.0 {
2374 return Err(runtime_err("Division by zero"));
2375 }
2376 Ok(VmValue::Float(a / b))
2377 }
2378 (VmValue::Int(a), VmValue::Float(b)) => {
2379 if *b == 0.0 {
2380 return Err(runtime_err("Division by zero"));
2381 }
2382 Ok(VmValue::Float(*a as f64 / b))
2383 }
2384 (VmValue::Float(a), VmValue::Int(b)) => {
2385 if *b == 0 {
2386 return Err(runtime_err("Division by zero"));
2387 }
2388 Ok(VmValue::Float(a / *b as f64))
2389 }
2390 #[cfg(feature = "gpu")]
2391 (VmValue::GpuTensor(a), VmValue::GpuTensor(b)) => {
2392 let a = a.clone();
2393 let b = b.clone();
2394 let ops = self.get_gpu_ops()?;
2395 let result = ops.div(&a, &b).map_err(runtime_err)?;
2396 Ok(VmValue::GpuTensor(Arc::new(result)))
2397 }
2398 #[cfg(feature = "gpu")]
2399 (VmValue::GpuTensor(_), VmValue::Tensor(_))
2400 | (VmValue::Tensor(_), VmValue::GpuTensor(_)) => {
2401 let lv = self.stack[base + b as usize].clone();
2402 let rv = self.stack[base + c as usize].clone();
2403 let a = self.ensure_gpu_tensor(&lv)?;
2404 let b_val = self.ensure_gpu_tensor(&rv)?;
2405 let ops = self.get_gpu_ops()?;
2406 let result = ops.div(&a, &b_val).map_err(runtime_err)?;
2407 Ok(VmValue::GpuTensor(Arc::new(result)))
2408 }
2409 #[cfg(feature = "native")]
2410 (VmValue::Tensor(a), VmValue::Tensor(b)) => {
2411 let result = a.div(b).map_err(|e| runtime_err(e.to_string()))?;
2412 Ok(VmValue::Tensor(Arc::new(result)))
2413 }
2414 (VmValue::Decimal(a), VmValue::Decimal(b)) => {
2415 if b.is_zero() {
2416 return Err(runtime_err("Division by zero"));
2417 }
2418 Ok(VmValue::Decimal(a / b))
2419 }
2420 (VmValue::Decimal(a), VmValue::Int(b)) => {
2421 if *b == 0 {
2422 return Err(runtime_err("Division by zero"));
2423 }
2424 Ok(VmValue::Decimal(a / rust_decimal::Decimal::from(*b)))
2425 }
2426 (VmValue::Int(a), VmValue::Decimal(b)) => {
2427 if b.is_zero() {
2428 return Err(runtime_err("Division by zero"));
2429 }
2430 Ok(VmValue::Decimal(rust_decimal::Decimal::from(*a) / b))
2431 }
2432 (VmValue::Decimal(a), VmValue::Float(b)) => {
2433 if *b == 0.0 {
2434 return Err(runtime_err("Division by zero"));
2435 }
2436 Ok(VmValue::Float(decimal_to_f64(a) / b))
2437 }
2438 (VmValue::Float(a), VmValue::Decimal(b)) => {
2439 if b.is_zero() {
2440 return Err(runtime_err("Division by zero"));
2441 }
2442 Ok(VmValue::Float(a / decimal_to_f64(b)))
2443 }
2444 _ => Err(runtime_err(format!(
2445 "Cannot apply `/` to {} and {}",
2446 left.type_name(),
2447 right.type_name()
2448 ))),
2449 }
2450 }
2451
2452 fn vm_mod(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2453 let left = &self.stack[base + b as usize];
2454 let right = &self.stack[base + c as usize];
2455 match (left, right) {
2456 (VmValue::Int(a), VmValue::Int(b)) => {
2457 if *b == 0 {
2458 return Err(runtime_err("Modulo by zero"));
2459 }
2460 Ok(VmValue::Int(a % b))
2461 }
2462 (VmValue::Float(a), VmValue::Float(b)) => {
2463 if *b == 0.0 {
2464 return Err(runtime_err("Modulo by zero"));
2465 }
2466 Ok(VmValue::Float(a % b))
2467 }
2468 (VmValue::Int(a), VmValue::Float(b)) => {
2469 if *b == 0.0 {
2470 return Err(runtime_err("Modulo by zero"));
2471 }
2472 Ok(VmValue::Float(*a as f64 % b))
2473 }
2474 (VmValue::Float(a), VmValue::Int(b)) => {
2475 if *b == 0 {
2476 return Err(runtime_err("Modulo by zero"));
2477 }
2478 Ok(VmValue::Float(a % *b as f64))
2479 }
2480 _ => Err(runtime_err(format!(
2481 "Cannot apply `%` to {} and {}",
2482 left.type_name(),
2483 right.type_name()
2484 ))),
2485 }
2486 }
2487
2488 fn vm_pow(&self, base: usize, b: u8, c: u8) -> Result<VmValue, TlError> {
2489 let left = &self.stack[base + b as usize];
2490 let right = &self.stack[base + c as usize];
2491 match (left, right) {
2492 (VmValue::Int(a), VmValue::Int(b)) => {
2493 if *b < 0 {
2494 return Ok(VmValue::Float((*a as f64).powi(*b as i32)));
2495 }
2496 match a.checked_pow(*b as u32) {
2497 Some(result) => Ok(VmValue::Int(result)),
2498 None => Ok(VmValue::Float((*a as f64).powf(*b as f64))),
2499 }
2500 }
2501 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
2502 (VmValue::Int(a), VmValue::Float(b)) => Ok(VmValue::Float((*a as f64).powf(*b))),
2503 (VmValue::Float(a), VmValue::Int(b)) => Ok(VmValue::Float(a.powf(*b as f64))),
2504 _ => Err(runtime_err(format!(
2505 "Cannot apply `**` to {} and {}",
2506 left.type_name(),
2507 right.type_name()
2508 ))),
2509 }
2510 }
2511
2512 fn vm_eq(&self, base: usize, b: u8, c: u8) -> bool {
2513 self.stack[base + b as usize] == self.stack[base + c as usize]
2514 }
2515
2516 fn vm_cmp(&self, base: usize, b: u8, c: u8) -> Result<Option<i8>, TlError> {
2517 let left = &self.stack[base + b as usize];
2518 let right = &self.stack[base + c as usize];
2519 match (left, right) {
2520 (VmValue::Int(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2521 (VmValue::Float(a), VmValue::Float(b)) => Ok(a.partial_cmp(b).map(|o| o as i8)),
2522 (VmValue::Int(a), VmValue::Float(b)) => {
2523 let fa = *a as f64;
2524 Ok(fa.partial_cmp(b).map(|o| o as i8))
2525 }
2526 (VmValue::Float(a), VmValue::Int(b)) => {
2527 let fb = *b as f64;
2528 Ok(a.partial_cmp(&fb).map(|o| o as i8))
2529 }
2530 (VmValue::String(a), VmValue::String(b)) => Ok(Some(a.cmp(b) as i8)),
2531 (VmValue::Decimal(a), VmValue::Decimal(b)) => Ok(Some(a.cmp(b) as i8)),
2532 (VmValue::Decimal(a), VmValue::Int(b)) => {
2533 Ok(Some(a.cmp(&rust_decimal::Decimal::from(*b)) as i8))
2534 }
2535 (VmValue::Int(a), VmValue::Decimal(b)) => {
2536 Ok(Some(rust_decimal::Decimal::from(*a).cmp(b) as i8))
2537 }
2538 (VmValue::DateTime(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2539 (VmValue::DateTime(a), VmValue::Int(b)) => Ok(Some(a.cmp(b) as i8)),
2540 (VmValue::Int(a), VmValue::DateTime(b)) => Ok(Some(a.cmp(b) as i8)),
2541 _ => Err(runtime_err(format!(
2542 "Cannot compare {} and {}",
2543 left.type_name(),
2544 right.type_name()
2545 ))),
2546 }
2547 }
2548
2549 fn check_permission(&self, perm: &str) -> Result<(), TlError> {
2552 if let Some(ref policy) = self.security_policy
2553 && !policy.check(perm)
2554 {
2555 return Err(runtime_err(format!("{perm} blocked by security policy")));
2556 }
2557 Ok(())
2558 }
2559
2560 pub fn call_builtin(
2563 &mut self,
2564 id: u16,
2565 args_base: usize,
2566 arg_count: usize,
2567 ) -> Result<VmValue, TlError> {
2568 let args: Vec<VmValue> = (0..arg_count)
2569 .map(|i| {
2570 let val = &self.stack[args_base + i];
2571 match val {
2573 VmValue::Ref(inner) => inner.as_ref().clone(),
2574 other => other.clone(),
2575 }
2576 })
2577 .collect();
2578
2579 let builtin_id: BuiltinId =
2580 BuiltinId::try_from(id).map_err(|v| runtime_err(format!("Invalid builtin id: {v}")))?;
2581
2582 match builtin_id {
2583 BuiltinId::Print | BuiltinId::Println => {
2584 let mut parts = Vec::new();
2585 for a in &args {
2586 #[cfg(feature = "native")]
2587 match a {
2588 VmValue::Table(t) => {
2589 let batches =
2590 self.engine().collect(t.df.clone()).map_err(runtime_err)?;
2591 let formatted =
2592 DataEngine::format_batches(&batches).map_err(runtime_err)?;
2593 parts.push(formatted);
2594 }
2595 _ => parts.push(format!("{a}")),
2596 }
2597 #[cfg(not(feature = "native"))]
2598 parts.push(format!("{a}"));
2599 }
2600 let line = parts.join(" ");
2601 println!("{line}");
2602 self.output.push(line);
2603 Ok(VmValue::None)
2604 }
2605 BuiltinId::Len => match args.first() {
2606 Some(VmValue::String(s)) => Ok(VmValue::Int(s.len() as i64)),
2607 Some(VmValue::List(l)) => Ok(VmValue::Int(l.len() as i64)),
2608 Some(VmValue::Map(pairs)) => Ok(VmValue::Int(pairs.len() as i64)),
2609 Some(VmValue::Set(items)) => Ok(VmValue::Int(items.len() as i64)),
2610 _ => Err(runtime_err("len() expects a string, list, map, or set")),
2611 },
2612 BuiltinId::Str => Ok(VmValue::String(Arc::from(
2613 args.first()
2614 .map(|v| format!("{v}"))
2615 .unwrap_or_default()
2616 .as_str(),
2617 ))),
2618 BuiltinId::Int => match args.first() {
2619 Some(VmValue::Float(f)) => Ok(VmValue::Int(*f as i64)),
2620 Some(VmValue::String(s)) => s
2621 .parse::<i64>()
2622 .map(VmValue::Int)
2623 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to int"))),
2624 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
2625 Some(VmValue::Bool(b)) => Ok(VmValue::Int(if *b { 1 } else { 0 })),
2626 _ => Err(runtime_err("int() expects a number, string, or bool")),
2627 },
2628 BuiltinId::Float => match args.first() {
2629 Some(VmValue::Int(n)) => Ok(VmValue::Float(*n as f64)),
2630 Some(VmValue::String(s)) => s
2631 .parse::<f64>()
2632 .map(VmValue::Float)
2633 .map_err(|_| runtime_err(format!("Cannot convert '{s}' to float"))),
2634 Some(VmValue::Float(n)) => Ok(VmValue::Float(*n)),
2635 Some(VmValue::Bool(b)) => Ok(VmValue::Float(if *b { 1.0 } else { 0.0 })),
2636 _ => Err(runtime_err("float() expects a number, string, or bool")),
2637 },
2638 BuiltinId::Abs => match args.first() {
2639 Some(VmValue::Int(n)) => Ok(VmValue::Int(n.abs())),
2640 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.abs())),
2641 _ => Err(runtime_err("abs() expects a number")),
2642 },
2643 BuiltinId::Min => {
2644 if args.len() == 2 {
2645 match (&args[0], &args[1]) {
2646 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.min(b))),
2647 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.min(*b))),
2648 _ => Err(runtime_err("min() expects two numbers")),
2649 }
2650 } else {
2651 Err(runtime_err("min() expects 2 arguments"))
2652 }
2653 }
2654 BuiltinId::Max => {
2655 if args.len() == 2 {
2656 match (&args[0], &args[1]) {
2657 (VmValue::Int(a), VmValue::Int(b)) => Ok(VmValue::Int(*a.max(b))),
2658 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.max(*b))),
2659 _ => Err(runtime_err("max() expects two numbers")),
2660 }
2661 } else {
2662 Err(runtime_err("max() expects 2 arguments"))
2663 }
2664 }
2665 BuiltinId::Range => {
2666 if args.len() == 1 {
2667 if let VmValue::Int(n) = &args[0] {
2668 if *n > 10_000_000 {
2669 return Err(runtime_err("range() size too large (max 10,000,000)"));
2670 }
2671 if *n < 0 {
2672 return Ok(VmValue::List(Box::default()));
2673 }
2674 Ok(VmValue::List(Box::new((0..*n).map(VmValue::Int).collect())))
2675 } else {
2676 Err(runtime_err("range() expects an integer"))
2677 }
2678 } else if args.len() == 2 {
2679 if let (VmValue::Int(start), VmValue::Int(end)) = (&args[0], &args[1]) {
2680 let size = (*end - *start).max(0);
2681 if size > 10_000_000 {
2682 return Err(runtime_err("range() size too large (max 10,000,000)"));
2683 }
2684 Ok(VmValue::List(Box::new(
2685 (*start..*end).map(VmValue::Int).collect(),
2686 )))
2687 } else {
2688 Err(runtime_err("range() expects integers"))
2689 }
2690 } else if args.len() == 3 {
2691 if let (VmValue::Int(start), VmValue::Int(end), VmValue::Int(step)) =
2692 (&args[0], &args[1], &args[2])
2693 {
2694 if *step == 0 {
2695 return Err(runtime_err("range() step cannot be zero"));
2696 }
2697 let mut result = Vec::new();
2698 let mut i = *start;
2699 if *step > 0 {
2700 while i < *end {
2701 result.push(VmValue::Int(i));
2702 i += step;
2703 }
2704 } else {
2705 while i > *end {
2706 result.push(VmValue::Int(i));
2707 i += step;
2708 }
2709 }
2710 Ok(VmValue::List(Box::new(result)))
2711 } else {
2712 Err(runtime_err("range() expects integers"))
2713 }
2714 } else {
2715 Err(runtime_err("range() expects 1, 2, or 3 arguments"))
2716 }
2717 }
2718 BuiltinId::Push => {
2719 if args.len() == 2 {
2720 if let VmValue::List(mut items) = args[0].clone() {
2721 items.push(args[1].clone());
2722 Ok(VmValue::List(items))
2723 } else {
2724 Err(runtime_err("push() first arg must be a list"))
2725 }
2726 } else {
2727 Err(runtime_err("push() expects 2 arguments"))
2728 }
2729 }
2730 BuiltinId::TypeOf => Ok(VmValue::String(Arc::from(
2731 args.first().map(|v| v.type_name()).unwrap_or("none"),
2732 ))),
2733 BuiltinId::Map => {
2734 if args.len() != 2 {
2735 return Err(runtime_err("map() expects 2 arguments (list, fn)"));
2736 }
2737 let items = match &args[0] {
2738 VmValue::List(items) => (**items).clone(),
2739 _ => return Err(runtime_err("map() first arg must be a list")),
2740 };
2741 let func = args[1].clone();
2742 #[cfg(feature = "native")]
2744 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2745 let proto = match &func {
2746 VmValue::Function(c) => c.prototype.clone(),
2747 _ => unreachable!(),
2748 };
2749 let result: Result<Vec<VmValue>, TlError> = items
2750 .into_par_iter()
2751 .map(|item| execute_pure_fn(&proto, &[item]))
2752 .collect();
2753 return Ok(VmValue::List(Box::new(result?)));
2754 }
2755 let mut result = Vec::new();
2756 for item in items {
2757 let val = self.call_vm_function(&func, &[item])?;
2758 result.push(val);
2759 }
2760 Ok(VmValue::List(Box::new(result)))
2761 }
2762 BuiltinId::Filter => {
2763 if args.len() != 2 {
2764 return Err(runtime_err("filter() expects 2 arguments (list, fn)"));
2765 }
2766 let items = match &args[0] {
2767 VmValue::List(items) => (**items).clone(),
2768 _ => return Err(runtime_err("filter() first arg must be a list")),
2769 };
2770 let func = args[1].clone();
2771 #[cfg(feature = "native")]
2773 if items.len() >= PARALLEL_THRESHOLD && is_pure_closure(&func) {
2774 let proto = match &func {
2775 VmValue::Function(c) => c.prototype.clone(),
2776 _ => unreachable!(),
2777 };
2778 let result: Result<Vec<VmValue>, TlError> = items
2779 .into_par_iter()
2780 .filter_map(|item| {
2781 match execute_pure_fn(&proto, std::slice::from_ref(&item)) {
2782 Ok(val) => {
2783 if val.is_truthy() {
2784 Some(Ok(item))
2785 } else {
2786 None
2787 }
2788 }
2789 Err(e) => Some(Err(e)),
2790 }
2791 })
2792 .collect();
2793 return Ok(VmValue::List(Box::new(result?)));
2794 }
2795 let mut result = Vec::new();
2796 for item in items {
2797 let val = self.call_vm_function(&func, std::slice::from_ref(&item))?;
2798 if val.is_truthy() {
2799 result.push(item);
2800 }
2801 }
2802 Ok(VmValue::List(Box::new(result)))
2803 }
2804 BuiltinId::Reduce | BuiltinId::Fold => {
2805 if args.len() != 3 {
2806 return Err(runtime_err(
2807 "reduce()/fold() expects 3 arguments (list, init, fn)",
2808 ));
2809 }
2810 let items = match &args[0] {
2811 VmValue::List(items) => (**items).clone(),
2812 _ => return Err(runtime_err("reduce() first arg must be a list")),
2813 };
2814 let mut acc = args[1].clone();
2815 let func = args[2].clone();
2816 for item in items {
2817 acc = self.call_vm_function(&func, &[acc, item])?;
2818 }
2819 Ok(acc)
2820 }
2821 BuiltinId::Sum => {
2822 if args.len() != 1 {
2823 return Err(runtime_err("sum() expects 1 argument (list)"));
2824 }
2825 let items = match &args[0] {
2826 VmValue::List(items) => items,
2827 _ => return Err(runtime_err("sum() expects a list")),
2828 };
2829 let has_float = items.iter().any(|v| matches!(v, VmValue::Float(_)));
2831 #[cfg(feature = "native")]
2832 if items.len() >= PARALLEL_THRESHOLD {
2833 if has_float {
2835 let total: f64 = items
2836 .par_iter()
2837 .map(|v| match v {
2838 VmValue::Int(n) => *n as f64,
2839 VmValue::Float(n) => *n,
2840 _ => 0.0,
2841 })
2842 .sum();
2843 return Ok(VmValue::Float(total));
2844 } else {
2845 let total: i64 = items
2846 .par_iter()
2847 .map(|v| match v {
2848 VmValue::Int(n) => *n,
2849 _ => 0,
2850 })
2851 .sum();
2852 return Ok(VmValue::Int(total));
2853 }
2854 }
2855 let mut total: i64 = 0;
2857 let mut is_float = false;
2858 let mut total_f: f64 = 0.0;
2859 for item in items.iter() {
2860 match item {
2861 VmValue::Int(n) => {
2862 if is_float {
2863 total_f += *n as f64;
2864 } else {
2865 total += n;
2866 }
2867 }
2868 VmValue::Float(n) => {
2869 if !is_float {
2870 total_f = total as f64;
2871 is_float = true;
2872 }
2873 total_f += n;
2874 }
2875 _ => return Err(runtime_err("sum() list must contain numbers")),
2876 }
2877 }
2878 if is_float {
2879 Ok(VmValue::Float(total_f))
2880 } else {
2881 Ok(VmValue::Int(total))
2882 }
2883 }
2884 BuiltinId::Any => {
2885 if args.len() != 2 {
2886 return Err(runtime_err("any() expects 2 arguments (list, fn)"));
2887 }
2888 let items = match &args[0] {
2889 VmValue::List(items) => (**items).clone(),
2890 _ => return Err(runtime_err("any() first arg must be a list")),
2891 };
2892 let func = args[1].clone();
2893 for item in items {
2894 let val = self.call_vm_function(&func, &[item])?;
2895 if val.is_truthy() {
2896 return Ok(VmValue::Bool(true));
2897 }
2898 }
2899 Ok(VmValue::Bool(false))
2900 }
2901 BuiltinId::All => {
2902 if args.len() != 2 {
2903 return Err(runtime_err("all() expects 2 arguments (list, fn)"));
2904 }
2905 let items = match &args[0] {
2906 VmValue::List(items) => (**items).clone(),
2907 _ => return Err(runtime_err("all() first arg must be a list")),
2908 };
2909 let func = args[1].clone();
2910 for item in items {
2911 let val = self.call_vm_function(&func, &[item])?;
2912 if !val.is_truthy() {
2913 return Ok(VmValue::Bool(false));
2914 }
2915 }
2916 Ok(VmValue::Bool(true))
2917 }
2918 #[cfg(feature = "native")]
2920 BuiltinId::ReadCsv => {
2921 if args.len() != 1 {
2922 return Err(runtime_err("read_csv() expects 1 argument (path)"));
2923 }
2924 let path = match &args[0] {
2925 VmValue::String(s) => s.to_string(),
2926 _ => return Err(runtime_err("read_csv() path must be a string")),
2927 };
2928 match self.engine().read_csv(&path) {
2929 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2930 Err(e) => {
2931 let msg = e.to_string();
2932 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2933 type_name: Arc::from("DataError"),
2934 variant: Arc::from("ParseError"),
2935 fields: vec![
2936 VmValue::String(Arc::from(msg.as_str())),
2937 VmValue::String(Arc::from(path.as_str())),
2938 ],
2939 })));
2940 Err(runtime_err(msg))
2941 }
2942 }
2943 }
2944 #[cfg(feature = "native")]
2945 BuiltinId::ReadParquet => {
2946 if args.len() != 1 {
2947 return Err(runtime_err("read_parquet() expects 1 argument (path)"));
2948 }
2949 let path = match &args[0] {
2950 VmValue::String(s) => s.to_string(),
2951 _ => return Err(runtime_err("read_parquet() path must be a string")),
2952 };
2953 match self.engine().read_parquet(&path) {
2954 Ok(df) => Ok(VmValue::Table(VmTable { df })),
2955 Err(e) => {
2956 let msg = e.to_string();
2957 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2958 type_name: Arc::from("DataError"),
2959 variant: Arc::from("ParseError"),
2960 fields: vec![
2961 VmValue::String(Arc::from(msg.as_str())),
2962 VmValue::String(Arc::from(path.as_str())),
2963 ],
2964 })));
2965 Err(runtime_err(msg))
2966 }
2967 }
2968 }
2969 #[cfg(feature = "native")]
2970 BuiltinId::WriteCsv => {
2971 if args.len() != 2 {
2972 return Err(runtime_err("write_csv() expects 2 arguments (table, path)"));
2973 }
2974 let df = match &args[0] {
2975 VmValue::Table(t) => t.df.clone(),
2976 _ => return Err(runtime_err("write_csv() first arg must be a table")),
2977 };
2978 let path = match &args[1] {
2979 VmValue::String(s) => s.to_string(),
2980 _ => return Err(runtime_err("write_csv() path must be a string")),
2981 };
2982 match self.engine().write_csv(df, &path) {
2983 Ok(_) => Ok(VmValue::None),
2984 Err(e) => {
2985 let msg = e.to_string();
2986 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
2987 type_name: Arc::from("DataError"),
2988 variant: Arc::from("ParseError"),
2989 fields: vec![
2990 VmValue::String(Arc::from(msg.as_str())),
2991 VmValue::String(Arc::from(path.as_str())),
2992 ],
2993 })));
2994 Err(runtime_err(msg))
2995 }
2996 }
2997 }
2998 #[cfg(feature = "native")]
2999 BuiltinId::WriteParquet => {
3000 if args.len() != 2 {
3001 return Err(runtime_err(
3002 "write_parquet() expects 2 arguments (table, path)",
3003 ));
3004 }
3005 let df = match &args[0] {
3006 VmValue::Table(t) => t.df.clone(),
3007 _ => return Err(runtime_err("write_parquet() first arg must be a table")),
3008 };
3009 let path = match &args[1] {
3010 VmValue::String(s) => s.to_string(),
3011 _ => return Err(runtime_err("write_parquet() path must be a string")),
3012 };
3013 match self.engine().write_parquet(df, &path) {
3014 Ok(_) => Ok(VmValue::None),
3015 Err(e) => {
3016 let msg = e.to_string();
3017 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3018 type_name: Arc::from("DataError"),
3019 variant: Arc::from("ParseError"),
3020 fields: vec![
3021 VmValue::String(Arc::from(msg.as_str())),
3022 VmValue::String(Arc::from(path.as_str())),
3023 ],
3024 })));
3025 Err(runtime_err(msg))
3026 }
3027 }
3028 }
3029 #[cfg(feature = "native")]
3030 BuiltinId::Collect => {
3031 if args.len() != 1 {
3032 return Err(runtime_err("collect() expects 1 argument (table)"));
3033 }
3034 let df = match &args[0] {
3035 VmValue::Table(t) => t.df.clone(),
3036 _ => return Err(runtime_err("collect() expects a table")),
3037 };
3038 let batches = self.engine().collect(df).map_err(runtime_err)?;
3039 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3040 Ok(VmValue::String(Arc::from(formatted.as_str())))
3041 }
3042 #[cfg(feature = "native")]
3043 BuiltinId::ToRows => {
3044 use tl_data::datafusion::arrow::array::{
3045 Array, BooleanArray, Float32Array, Float64Array, Int32Array, Int64Array,
3046 LargeStringArray, StringArray, UInt32Array, UInt64Array,
3047 };
3048 if args.len() != 1 {
3049 return Err(runtime_err("to_rows() expects 1 argument (table)"));
3050 }
3051 let df = match &args[0] {
3052 VmValue::Table(t) => t.df.clone(),
3053 _ => return Err(runtime_err("to_rows() expects a table")),
3054 };
3055 let batches = self.engine().collect(df).map_err(runtime_err)?;
3056 let mut rows: Vec<VmValue> = Vec::new();
3057 for batch in &batches {
3058 let schema = batch.schema();
3059 let num_rows = batch.num_rows();
3060 for row_idx in 0..num_rows {
3061 let mut map: Vec<(Arc<str>, VmValue)> = Vec::new();
3062 for col_idx in 0..batch.num_columns() {
3063 let col_name: Arc<str> =
3064 Arc::from(schema.field(col_idx).name().as_str());
3065 let col = batch.column(col_idx);
3066 let val = if col.is_null(row_idx) {
3067 VmValue::None
3068 } else if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
3069 VmValue::Float(arr.value(row_idx))
3070 } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
3071 VmValue::Float(arr.value(row_idx) as f64)
3072 } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
3073 VmValue::Int(arr.value(row_idx))
3074 } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
3075 VmValue::Int(arr.value(row_idx) as i64)
3076 } else if let Some(arr) = col.as_any().downcast_ref::<UInt64Array>() {
3077 VmValue::Int(arr.value(row_idx) as i64)
3078 } else if let Some(arr) = col.as_any().downcast_ref::<UInt32Array>() {
3079 VmValue::Int(arr.value(row_idx) as i64)
3080 } else if let Some(arr) = col.as_any().downcast_ref::<StringArray>() {
3081 VmValue::String(Arc::from(arr.value(row_idx)))
3082 } else if let Some(arr) =
3083 col.as_any().downcast_ref::<LargeStringArray>()
3084 {
3085 VmValue::String(Arc::from(arr.value(row_idx)))
3086 } else if let Some(arr) = col.as_any().downcast_ref::<BooleanArray>() {
3087 VmValue::Bool(arr.value(row_idx))
3088 } else {
3089 VmValue::String(Arc::from(
3090 format!("{:?}", col.data_type()).as_str(),
3091 ))
3092 };
3093 map.push((col_name, val));
3094 }
3095 rows.push(VmValue::Map(Box::new(map)));
3096 }
3097 }
3098 Ok(VmValue::List(Box::new(rows)))
3099 }
3100 #[cfg(feature = "native")]
3101 BuiltinId::Show => {
3102 let df = match args.first() {
3103 Some(VmValue::Table(t)) => t.df.clone(),
3104 _ => return Err(runtime_err("show() expects a table")),
3105 };
3106 let limit = match args.get(1) {
3107 Some(VmValue::Int(n)) => *n as usize,
3108 None => 20,
3109 _ => return Err(runtime_err("show() second arg must be an int")),
3110 };
3111 let limited = df
3112 .limit(0, Some(limit))
3113 .map_err(|e| runtime_err(format!("{e}")))?;
3114 let batches = self.engine().collect(limited).map_err(runtime_err)?;
3115 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
3116 println!("{formatted}");
3117 self.output.push(formatted);
3118 Ok(VmValue::None)
3119 }
3120 #[cfg(feature = "native")]
3121 BuiltinId::Describe => {
3122 if args.len() != 1 {
3123 return Err(runtime_err("describe() expects 1 argument (table)"));
3124 }
3125 let df = match &args[0] {
3126 VmValue::Table(t) => t.df.clone(),
3127 _ => return Err(runtime_err("describe() expects a table")),
3128 };
3129 let schema = df.schema();
3130 let mut lines = Vec::new();
3131 lines.push("Columns:".to_string());
3132 for (qualifier, field) in schema.iter() {
3133 let prefix = match qualifier {
3134 Some(q) => format!("{q}."),
3135 None => String::new(),
3136 };
3137 lines.push(format!(
3138 " {}{}: {}",
3139 prefix,
3140 field.name(),
3141 field.data_type()
3142 ));
3143 }
3144 let output = lines.join("\n");
3145 println!("{output}");
3146 self.output.push(output.clone());
3147 Ok(VmValue::String(Arc::from(output.as_str())))
3148 }
3149 #[cfg(feature = "native")]
3150 BuiltinId::Head => {
3151 if args.is_empty() {
3152 return Err(runtime_err("head() expects at least 1 argument (table)"));
3153 }
3154 let df = match &args[0] {
3155 VmValue::Table(t) => t.df.clone(),
3156 _ => return Err(runtime_err("head() first arg must be a table")),
3157 };
3158 let n = match args.get(1) {
3159 Some(VmValue::Int(n)) => *n as usize,
3160 None => 10,
3161 _ => return Err(runtime_err("head() second arg must be an int")),
3162 };
3163 let limited = df
3164 .limit(0, Some(n))
3165 .map_err(|e| runtime_err(format!("{e}")))?;
3166 Ok(VmValue::Table(VmTable { df: limited }))
3167 }
3168 #[cfg(feature = "native")]
3169 BuiltinId::Postgres => {
3170 if args.len() != 2 {
3171 return Err(runtime_err(
3172 "postgres() expects 2 arguments (conn_str, table_name)",
3173 ));
3174 }
3175 let conn_str = match &args[0] {
3176 VmValue::String(s) => s.to_string(),
3177 _ => return Err(runtime_err("postgres() conn_str must be a string")),
3178 };
3179 let table_name = match &args[1] {
3180 VmValue::String(s) => s.to_string(),
3181 _ => return Err(runtime_err("postgres() table_name must be a string")),
3182 };
3183 let conn_str = resolve_tl_config_connection(&conn_str);
3184 match self.engine().read_postgres(&conn_str, &table_name) {
3185 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3186 Err(e) => {
3187 let msg = e.to_string();
3188 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3189 type_name: Arc::from("ConnectorError"),
3190 variant: Arc::from("QueryError"),
3191 fields: vec![
3192 VmValue::String(Arc::from(msg.as_str())),
3193 VmValue::String(Arc::from("postgres")),
3194 ],
3195 })));
3196 Err(runtime_err(msg))
3197 }
3198 }
3199 }
3200 #[cfg(feature = "native")]
3201 BuiltinId::WritePostgres => {
3202 if args.len() < 3 {
3203 return Err(runtime_err(
3204 "write_postgres() expects (table, conn_str, table_name, [mode])",
3205 ));
3206 }
3207 self.check_permission("connector:postgres")?;
3209 let df = match &args[0] {
3210 VmValue::Table(t) => t.df.clone(),
3211 _ => return Err(runtime_err("write_postgres() first arg must be a table")),
3212 };
3213 let conn_str = match &args[1] {
3214 VmValue::String(s) => resolve_tl_config_connection(s),
3215 _ => return Err(runtime_err("write_postgres() conn_str must be a string")),
3216 };
3217 let table_name = match &args[2] {
3218 VmValue::String(s) => s.to_string(),
3219 _ => return Err(runtime_err("write_postgres() table_name must be a string")),
3220 };
3221 let mode = match args.get(3) {
3222 None | Some(VmValue::None) => "create".to_string(),
3223 Some(VmValue::String(s)) => s.to_string(),
3224 _ => return Err(runtime_err("write_postgres() mode must be a string")),
3225 };
3226 let n = self
3227 .engine()
3228 .write_postgres(df, &conn_str, &table_name, &mode)
3229 .map_err(runtime_err)?;
3230 Ok(VmValue::Int(n as i64))
3231 }
3232 #[cfg(feature = "native")]
3233 BuiltinId::WriteRedshift => {
3234 if args.len() < 3 {
3235 return Err(runtime_err(
3236 "write_redshift() expects (table, conn_str, table_name, [mode])",
3237 ));
3238 }
3239 self.check_permission("connector:redshift")?;
3240 let df = match &args[0] {
3241 VmValue::Table(t) => t.df.clone(),
3242 _ => return Err(runtime_err("write_redshift() first arg must be a table")),
3243 };
3244 let conn_str = match &args[1] {
3245 VmValue::String(s) => resolve_tl_config_connection(s),
3246 _ => return Err(runtime_err("write_redshift() conn_str must be a string")),
3247 };
3248 let table_name = match &args[2] {
3249 VmValue::String(s) => s.to_string(),
3250 _ => return Err(runtime_err("write_redshift() table_name must be a string")),
3251 };
3252 let mode = match args.get(3) {
3253 None | Some(VmValue::None) => "create".to_string(),
3254 Some(VmValue::String(s)) => s.to_string(),
3255 _ => return Err(runtime_err("write_redshift() mode must be a string")),
3256 };
3257 let n = self
3258 .engine()
3259 .write_redshift(df, &conn_str, &table_name, &mode)
3260 .map_err(runtime_err)?;
3261 Ok(VmValue::Int(n as i64))
3262 }
3263 #[cfg(feature = "native")]
3264 BuiltinId::WriteMysql => {
3265 #[cfg(feature = "mysql")]
3266 {
3267 if args.len() < 3 {
3268 return Err(runtime_err(
3269 "write_mysql() expects (table, conn_str, table_name, [mode])",
3270 ));
3271 }
3272 self.check_permission("connector:mysql")?;
3273 let df = match &args[0] {
3274 VmValue::Table(t) => t.df.clone(),
3275 _ => return Err(runtime_err("write_mysql() first arg must be a table")),
3276 };
3277 let conn_str = match &args[1] {
3278 VmValue::String(s) => resolve_tl_config_connection(s),
3279 _ => return Err(runtime_err("write_mysql() conn_str must be a string")),
3280 };
3281 let table_name = match &args[2] {
3282 VmValue::String(s) => s.to_string(),
3283 _ => return Err(runtime_err("write_mysql() table_name must be a string")),
3284 };
3285 let mode = match args.get(3) {
3286 None | Some(VmValue::None) => "create".to_string(),
3287 Some(VmValue::String(s)) => s.to_string(),
3288 _ => return Err(runtime_err("write_mysql() mode must be a string")),
3289 };
3290 let n = self
3291 .engine()
3292 .write_mysql(df, &conn_str, &table_name, &mode)
3293 .map_err(runtime_err)?;
3294 Ok(VmValue::Int(n as i64))
3295 }
3296 #[cfg(not(feature = "mysql"))]
3297 Err(runtime_err("write_mysql() requires the 'mysql' feature"))
3298 }
3299 #[cfg(feature = "native")]
3300 BuiltinId::WriteClickHouse => {
3301 #[cfg(feature = "clickhouse")]
3302 {
3303 if args.len() < 3 {
3304 return Err(runtime_err(
3305 "write_clickhouse() expects (table, url, table_name, [mode])",
3306 ));
3307 }
3308 self.check_permission("connector:clickhouse")?;
3309 let df = match &args[0] {
3310 VmValue::Table(t) => t.df.clone(),
3311 _ => {
3312 return Err(runtime_err(
3313 "write_clickhouse() first arg must be a table",
3314 ));
3315 }
3316 };
3317 let url = match &args[1] {
3318 VmValue::String(s) => resolve_tl_config_connection(s),
3319 _ => return Err(runtime_err("write_clickhouse() url must be a string")),
3320 };
3321 let table_name = match &args[2] {
3322 VmValue::String(s) => s.to_string(),
3323 _ => {
3324 return Err(runtime_err(
3325 "write_clickhouse() table_name must be a string",
3326 ));
3327 }
3328 };
3329 let mode = match args.get(3) {
3330 None | Some(VmValue::None) => "create".to_string(),
3331 Some(VmValue::String(s)) => s.to_string(),
3332 _ => return Err(runtime_err("write_clickhouse() mode must be a string")),
3333 };
3334 let n = self
3335 .engine()
3336 .write_clickhouse(df, &url, &table_name, &mode)
3337 .map_err(runtime_err)?;
3338 Ok(VmValue::Int(n as i64))
3339 }
3340 #[cfg(not(feature = "clickhouse"))]
3341 Err(runtime_err(
3342 "write_clickhouse() requires the 'clickhouse' feature",
3343 ))
3344 }
3345 #[cfg(feature = "native")]
3346 BuiltinId::WriteSnowflake => {
3347 #[cfg(feature = "snowflake")]
3348 {
3349 let (df, cfg, table_name, mode) = write_args(&args, "write_snowflake")?;
3350 self.check_permission("connector:snowflake")?;
3351 let n = self
3352 .engine()
3353 .write_snowflake(df, &cfg, &table_name, &mode)
3354 .map_err(runtime_err)?;
3355 Ok(VmValue::Int(n as i64))
3356 }
3357 #[cfg(not(feature = "snowflake"))]
3358 Err(runtime_err(
3359 "write_snowflake() requires the 'snowflake' feature",
3360 ))
3361 }
3362 #[cfg(feature = "native")]
3363 BuiltinId::WriteBigQuery => {
3364 #[cfg(feature = "bigquery")]
3365 {
3366 let (df, cfg, table_name, mode) = write_args(&args, "write_bigquery")?;
3367 self.check_permission("connector:bigquery")?;
3368 let n = self
3369 .engine()
3370 .write_bigquery(df, &cfg, &table_name, &mode)
3371 .map_err(runtime_err)?;
3372 Ok(VmValue::Int(n as i64))
3373 }
3374 #[cfg(not(feature = "bigquery"))]
3375 Err(runtime_err(
3376 "write_bigquery() requires the 'bigquery' feature",
3377 ))
3378 }
3379 #[cfg(feature = "native")]
3380 BuiltinId::WriteDatabricks => {
3381 #[cfg(feature = "databricks")]
3382 {
3383 let (df, cfg, table_name, mode) = write_args(&args, "write_databricks")?;
3384 self.check_permission("connector:databricks")?;
3385 let n = self
3386 .engine()
3387 .write_databricks(df, &cfg, &table_name, &mode)
3388 .map_err(runtime_err)?;
3389 Ok(VmValue::Int(n as i64))
3390 }
3391 #[cfg(not(feature = "databricks"))]
3392 Err(runtime_err(
3393 "write_databricks() requires the 'databricks' feature",
3394 ))
3395 }
3396 #[cfg(feature = "native")]
3397 BuiltinId::WriteMssql => {
3398 #[cfg(feature = "mssql")]
3399 {
3400 if args.len() < 3 {
3401 return Err(runtime_err(
3402 "write_mssql() expects (table, conn_str, table_name, [mode])",
3403 ));
3404 }
3405 self.check_permission("connector:mssql")?;
3406 let df = match &args[0] {
3407 VmValue::Table(t) => t.df.clone(),
3408 _ => return Err(runtime_err("write_mssql() first arg must be a table")),
3409 };
3410 let conn_str = match &args[1] {
3411 VmValue::String(s) => resolve_tl_config_connection(s),
3412 _ => return Err(runtime_err("write_mssql() conn_str must be a string")),
3413 };
3414 let table_name = match &args[2] {
3415 VmValue::String(s) => s.to_string(),
3416 _ => return Err(runtime_err("write_mssql() table_name must be a string")),
3417 };
3418 let mode = match args.get(3) {
3419 None | Some(VmValue::None) => "create".to_string(),
3420 Some(VmValue::String(s)) => s.to_string(),
3421 _ => return Err(runtime_err("write_mssql() mode must be a string")),
3422 };
3423 let n = self
3424 .engine()
3425 .write_mssql(df, &conn_str, &table_name, &mode)
3426 .map_err(runtime_err)?;
3427 Ok(VmValue::Int(n as i64))
3428 }
3429 #[cfg(not(feature = "mssql"))]
3430 Err(runtime_err("write_mssql() requires the 'mssql' feature"))
3431 }
3432 #[cfg(feature = "native")]
3433 BuiltinId::WriteMongo => {
3434 #[cfg(feature = "mongodb")]
3435 {
3436 if args.len() < 4 {
3437 return Err(runtime_err(
3438 "write_mongo() expects (table, conn_str, database, collection, [mode])",
3439 ));
3440 }
3441 self.check_permission("connector:mongodb")?;
3442 let df = match &args[0] {
3443 VmValue::Table(t) => t.df.clone(),
3444 _ => return Err(runtime_err("write_mongo() first arg must be a table")),
3445 };
3446 let conn_str = match &args[1] {
3447 VmValue::String(s) => resolve_tl_config_connection(s),
3448 _ => return Err(runtime_err("write_mongo() conn_str must be a string")),
3449 };
3450 let database = match &args[2] {
3451 VmValue::String(s) => s.to_string(),
3452 _ => return Err(runtime_err("write_mongo() database must be a string")),
3453 };
3454 let collection = match &args[3] {
3455 VmValue::String(s) => s.to_string(),
3456 _ => return Err(runtime_err("write_mongo() collection must be a string")),
3457 };
3458 let mode = match args.get(4) {
3459 None | Some(VmValue::None) => "create".to_string(),
3460 Some(VmValue::String(s)) => s.to_string(),
3461 _ => return Err(runtime_err("write_mongo() mode must be a string")),
3462 };
3463 let n = self
3464 .engine()
3465 .write_mongo(df, &conn_str, &database, &collection, &mode)
3466 .map_err(runtime_err)?;
3467 Ok(VmValue::Int(n as i64))
3468 }
3469 #[cfg(not(feature = "mongodb"))]
3470 Err(runtime_err("write_mongo() requires the 'mongodb' feature"))
3471 }
3472 #[cfg(feature = "native")]
3473 BuiltinId::PostgresQuery => {
3474 if args.len() != 2 {
3475 return Err(runtime_err(
3476 "postgres_query() expects 2 arguments (conn_str, query)",
3477 ));
3478 }
3479 let conn_str = match &args[0] {
3480 VmValue::String(s) => s.to_string(),
3481 _ => return Err(runtime_err("postgres_query() conn_str must be a string")),
3482 };
3483 let query = match &args[1] {
3484 VmValue::String(s) => s.to_string(),
3485 _ => return Err(runtime_err("postgres_query() query must be a string")),
3486 };
3487 let conn_str = resolve_tl_config_connection(&conn_str);
3488 match self
3489 .engine()
3490 .query_postgres(&conn_str, &query, "__pg_query_result")
3491 {
3492 Ok(df) => Ok(VmValue::Table(VmTable { df })),
3493 Err(e) => {
3494 let msg = e.to_string();
3495 self.thrown_value = Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3496 type_name: Arc::from("ConnectorError"),
3497 variant: Arc::from("QueryError"),
3498 fields: vec![
3499 VmValue::String(Arc::from(msg.as_str())),
3500 VmValue::String(Arc::from("postgres")),
3501 ],
3502 })));
3503 Err(runtime_err(msg))
3504 }
3505 }
3506 }
3507 BuiltinId::TlConfigResolve => {
3508 if args.len() != 1 {
3509 return Err(runtime_err("tl_config_resolve() expects 1 argument (name)"));
3510 }
3511 let name = match &args[0] {
3512 VmValue::String(s) => s.to_string(),
3513 _ => return Err(runtime_err("tl_config_resolve() name must be a string")),
3514 };
3515 let resolved = resolve_tl_config_connection(&name);
3516 Ok(VmValue::String(Arc::from(resolved.as_str())))
3517 }
3518 #[cfg(not(feature = "native"))]
3519 BuiltinId::ReadCsv
3520 | BuiltinId::ReadParquet
3521 | BuiltinId::WriteCsv
3522 | BuiltinId::WriteParquet
3523 | BuiltinId::Collect
3524 | BuiltinId::ToRows
3525 | BuiltinId::Show
3526 | BuiltinId::Describe
3527 | BuiltinId::Head
3528 | BuiltinId::Postgres
3529 | BuiltinId::WritePostgres
3530 | BuiltinId::WriteRedshift
3531 | BuiltinId::WriteMysql
3532 | BuiltinId::WriteClickHouse
3533 | BuiltinId::WriteSnowflake
3534 | BuiltinId::WriteBigQuery
3535 | BuiltinId::WriteDatabricks
3536 | BuiltinId::WriteMssql
3537 | BuiltinId::WriteMongo
3538 | BuiltinId::PostgresQuery => Err(runtime_err("Data operations not available in WASM")),
3539 #[cfg(feature = "native")]
3541 BuiltinId::Tensor => {
3542 if args.is_empty() {
3543 return Err(runtime_err("tensor() expects at least 1 argument"));
3544 }
3545 let data = self.vmvalue_to_f64_list(&args[0])?;
3546 let shape = if args.len() > 1 {
3547 self.vmvalue_to_usize_list(&args[1])?
3548 } else {
3549 vec![data.len()]
3550 };
3551 let t = tl_ai::TlTensor::from_vec(data, &shape)
3552 .map_err(|e| runtime_err(e.to_string()))?;
3553 Ok(VmValue::Tensor(Arc::new(t)))
3554 }
3555 #[cfg(feature = "native")]
3556 BuiltinId::TensorZeros => {
3557 if args.is_empty() {
3558 return Err(runtime_err("tensor_zeros() expects 1 argument (shape)"));
3559 }
3560 let shape = self.vmvalue_to_usize_list(&args[0])?;
3561 let t = tl_ai::TlTensor::zeros(&shape);
3562 Ok(VmValue::Tensor(Arc::new(t)))
3563 }
3564 #[cfg(feature = "native")]
3565 BuiltinId::TensorOnes => {
3566 if args.is_empty() {
3567 return Err(runtime_err("tensor_ones() expects 1 argument (shape)"));
3568 }
3569 let shape = self.vmvalue_to_usize_list(&args[0])?;
3570 let t = tl_ai::TlTensor::ones(&shape);
3571 Ok(VmValue::Tensor(Arc::new(t)))
3572 }
3573 #[cfg(feature = "native")]
3574 BuiltinId::TensorShape => match args.first() {
3575 Some(VmValue::Tensor(t)) => {
3576 let shape: Vec<VmValue> =
3577 t.shape().iter().map(|&d| VmValue::Int(d as i64)).collect();
3578 Ok(VmValue::List(Box::new(shape)))
3579 }
3580 _ => Err(runtime_err("tensor_shape() expects a tensor")),
3581 },
3582 #[cfg(feature = "native")]
3583 BuiltinId::TensorReshape => {
3584 if args.len() != 2 {
3585 return Err(runtime_err(
3586 "tensor_reshape() expects 2 arguments (tensor, shape)",
3587 ));
3588 }
3589 let t = match &args[0] {
3590 VmValue::Tensor(t) => (**t).clone(),
3591 _ => return Err(runtime_err("tensor_reshape() first arg must be a tensor")),
3592 };
3593 let shape = self.vmvalue_to_usize_list(&args[1])?;
3594 let reshaped = t.reshape(&shape).map_err(|e| runtime_err(e.to_string()))?;
3595 Ok(VmValue::Tensor(Arc::new(reshaped)))
3596 }
3597 #[cfg(feature = "native")]
3598 BuiltinId::TensorTranspose => match args.first() {
3599 Some(VmValue::Tensor(t)) => {
3600 let transposed = t.transpose().map_err(|e| runtime_err(e.to_string()))?;
3601 Ok(VmValue::Tensor(Arc::new(transposed)))
3602 }
3603 _ => Err(runtime_err("tensor_transpose() expects a tensor")),
3604 },
3605 #[cfg(feature = "native")]
3606 BuiltinId::TensorSum => match args.first() {
3607 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.sum())),
3608 _ => Err(runtime_err("tensor_sum() expects a tensor")),
3609 },
3610 #[cfg(feature = "native")]
3611 BuiltinId::TensorMean => match args.first() {
3612 Some(VmValue::Tensor(t)) => Ok(VmValue::Float(t.mean())),
3613 _ => Err(runtime_err("tensor_mean() expects a tensor")),
3614 },
3615 #[cfg(feature = "native")]
3616 BuiltinId::TensorDot => {
3617 if args.len() != 2 {
3618 return Err(runtime_err("tensor_dot() expects 2 arguments"));
3619 }
3620 let a_t = match &args[0] {
3621 VmValue::Tensor(t) => t,
3622 _ => return Err(runtime_err("tensor_dot() first arg must be a tensor")),
3623 };
3624 let b_t = match &args[1] {
3625 VmValue::Tensor(t) => t,
3626 _ => return Err(runtime_err("tensor_dot() second arg must be a tensor")),
3627 };
3628 let result = a_t.dot(b_t).map_err(|e| runtime_err(e.to_string()))?;
3629 Ok(VmValue::Tensor(Arc::new(result)))
3630 }
3631 #[cfg(feature = "native")]
3632 BuiltinId::Predict => {
3633 if args.len() < 2 {
3634 return Err(runtime_err(
3635 "predict() expects at least 2 arguments (model, input)",
3636 ));
3637 }
3638 let model = match &args[0] {
3639 VmValue::Model(m) => (**m).clone(),
3640 _ => return Err(runtime_err("predict() first arg must be a model")),
3641 };
3642 let input = match &args[1] {
3643 VmValue::Tensor(t) => (**t).clone(),
3644 _ => return Err(runtime_err("predict() second arg must be a tensor")),
3645 };
3646 let result =
3647 tl_ai::predict(&model, &input).map_err(|e| runtime_err(e.to_string()))?;
3648 Ok(VmValue::Tensor(Arc::new(result)))
3649 }
3650 #[cfg(feature = "native")]
3651 BuiltinId::Similarity => {
3652 if args.len() != 2 {
3653 return Err(runtime_err("similarity() expects 2 arguments"));
3654 }
3655 let a_t = match &args[0] {
3656 VmValue::Tensor(t) => t,
3657 _ => return Err(runtime_err("similarity() first arg must be a tensor")),
3658 };
3659 let b_t = match &args[1] {
3660 VmValue::Tensor(t) => t,
3661 _ => return Err(runtime_err("similarity() second arg must be a tensor")),
3662 };
3663 let sim = tl_ai::similarity(a_t, b_t).map_err(|e| runtime_err(e.to_string()))?;
3664 Ok(VmValue::Float(sim))
3665 }
3666 #[cfg(feature = "native")]
3667 BuiltinId::AiComplete => {
3668 if args.is_empty() {
3669 return Err(runtime_err(
3670 "ai_complete() expects at least 1 argument (prompt)",
3671 ));
3672 }
3673 let prompt = match &args[0] {
3674 VmValue::String(s) => s.to_string(),
3675 _ => return Err(runtime_err("ai_complete() first arg must be a string")),
3676 };
3677 let model = match args.get(1) {
3678 Some(VmValue::String(s)) => Some(s.to_string()),
3679 _ => None,
3680 };
3681 let result = tl_ai::ai_complete(&prompt, model.as_deref(), None, None)
3682 .map_err(|e| runtime_err(e.to_string()))?;
3683 Ok(VmValue::String(Arc::from(result.as_str())))
3684 }
3685 #[cfg(feature = "native")]
3686 BuiltinId::AiChat => {
3687 if args.is_empty() {
3688 return Err(runtime_err("ai_chat() expects at least 1 argument (model)"));
3689 }
3690 let model = match &args[0] {
3691 VmValue::String(s) => s.to_string(),
3692 _ => return Err(runtime_err("ai_chat() first arg must be a string (model)")),
3693 };
3694 let system = match args.get(1) {
3695 Some(VmValue::String(s)) => Some(s.to_string()),
3696 _ => None,
3697 };
3698 let messages: Vec<(String, String)> = if let Some(VmValue::List(msgs)) = args.get(2)
3699 {
3700 msgs.chunks(2)
3701 .filter_map(|chunk| {
3702 if chunk.len() == 2
3703 && let (VmValue::String(role), VmValue::String(content)) =
3704 (&chunk[0], &chunk[1])
3705 {
3706 return Some((role.to_string(), content.to_string()));
3707 }
3708 None
3709 })
3710 .collect()
3711 } else {
3712 Vec::new()
3713 };
3714 let result = tl_ai::ai_chat(&model, system.as_deref(), &messages)
3715 .map_err(|e| runtime_err(e.to_string()))?;
3716 Ok(VmValue::String(Arc::from(result.as_str())))
3717 }
3718 #[cfg(feature = "native")]
3719 BuiltinId::ModelSave => {
3720 if args.len() != 2 {
3721 return Err(runtime_err(
3722 "model_save() expects 2 arguments (model, path)",
3723 ));
3724 }
3725 let model = match &args[0] {
3726 VmValue::Model(m) => m,
3727 _ => return Err(runtime_err("model_save() first arg must be a model")),
3728 };
3729 let path = match &args[1] {
3730 VmValue::String(s) => s.to_string(),
3731 _ => return Err(runtime_err("model_save() second arg must be a string path")),
3732 };
3733 model
3734 .save(std::path::Path::new(&path))
3735 .map_err(|e| runtime_err(e.to_string()))?;
3736 Ok(VmValue::None)
3737 }
3738 #[cfg(feature = "native")]
3739 BuiltinId::ModelLoad => {
3740 if args.is_empty() {
3741 return Err(runtime_err("model_load() expects 1 argument (path)"));
3742 }
3743 let path = match &args[0] {
3744 VmValue::String(s) => s.to_string(),
3745 _ => return Err(runtime_err("model_load() arg must be a string path")),
3746 };
3747 let model = tl_ai::TlModel::load(std::path::Path::new(&path))
3748 .map_err(|e| runtime_err(e.to_string()))?;
3749 Ok(VmValue::Model(Arc::new(model)))
3750 }
3751 #[cfg(feature = "native")]
3752 BuiltinId::ModelRegister => {
3753 if args.len() != 2 {
3754 return Err(runtime_err(
3755 "model_register() expects 2 arguments (name, model)",
3756 ));
3757 }
3758 let name = match &args[0] {
3759 VmValue::String(s) => s.to_string(),
3760 _ => return Err(runtime_err("model_register() first arg must be a string")),
3761 };
3762 let model = match &args[1] {
3763 VmValue::Model(m) => (**m).clone(),
3764 _ => return Err(runtime_err("model_register() second arg must be a model")),
3765 };
3766 let registry = tl_ai::ModelRegistry::default_location();
3767 registry
3768 .register(&name, &model)
3769 .map_err(|e| runtime_err(e.to_string()))?;
3770 Ok(VmValue::None)
3771 }
3772 #[cfg(feature = "native")]
3773 BuiltinId::ModelList => {
3774 let registry = tl_ai::ModelRegistry::default_location();
3775 let names = registry.list();
3776 let items: Vec<VmValue> = names
3777 .into_iter()
3778 .map(|n: String| VmValue::String(Arc::from(n.as_str())))
3779 .collect();
3780 Ok(VmValue::List(Box::new(items)))
3781 }
3782 #[cfg(feature = "native")]
3783 BuiltinId::ModelGet => {
3784 if args.is_empty() {
3785 return Err(runtime_err("model_get() expects 1 argument (name)"));
3786 }
3787 let name = match &args[0] {
3788 VmValue::String(s) => s.to_string(),
3789 _ => return Err(runtime_err("model_get() arg must be a string")),
3790 };
3791 let registry = tl_ai::ModelRegistry::default_location();
3792 match registry.get(&name) {
3793 Ok(m) => Ok(VmValue::Model(Arc::new(m))),
3794 Err(_) => Ok(VmValue::None),
3795 }
3796 }
3797 #[cfg(not(feature = "native"))]
3798 BuiltinId::Tensor
3799 | BuiltinId::TensorZeros
3800 | BuiltinId::TensorOnes
3801 | BuiltinId::TensorShape
3802 | BuiltinId::TensorReshape
3803 | BuiltinId::TensorTranspose
3804 | BuiltinId::TensorSum
3805 | BuiltinId::TensorMean
3806 | BuiltinId::TensorDot
3807 | BuiltinId::Predict
3808 | BuiltinId::Similarity
3809 | BuiltinId::AiComplete
3810 | BuiltinId::AiChat
3811 | BuiltinId::ModelSave
3812 | BuiltinId::ModelLoad
3813 | BuiltinId::ModelRegister
3814 | BuiltinId::ModelList
3815 | BuiltinId::ModelGet => Err(runtime_err("AI/ML operations not available in WASM")),
3816 #[cfg(feature = "native")]
3818 BuiltinId::AlertSlack => {
3819 if args.len() < 2 {
3820 return Err(runtime_err("alert_slack(url, msg) requires 2 args"));
3821 }
3822 let url = match &args[0] {
3823 VmValue::String(s) => s.to_string(),
3824 _ => return Err(runtime_err("alert_slack: url must be a string")),
3825 };
3826 let msg = format!("{}", args[1]);
3827 tl_stream::send_alert(&tl_stream::AlertTarget::Slack(url), &msg)
3828 .map_err(|e| runtime_err(&e))?;
3829 Ok(VmValue::None)
3830 }
3831 #[cfg(feature = "native")]
3832 BuiltinId::AlertWebhook => {
3833 if args.len() < 2 {
3834 return Err(runtime_err("alert_webhook(url, msg) requires 2 args"));
3835 }
3836 let url = match &args[0] {
3837 VmValue::String(s) => s.to_string(),
3838 _ => return Err(runtime_err("alert_webhook: url must be a string")),
3839 };
3840 let msg = format!("{}", args[1]);
3841 tl_stream::send_alert(&tl_stream::AlertTarget::Webhook(url), &msg)
3842 .map_err(|e| runtime_err(&e))?;
3843 Ok(VmValue::None)
3844 }
3845 #[cfg(feature = "native")]
3846 BuiltinId::Emit => {
3847 if args.is_empty() {
3848 return Err(runtime_err("emit() requires at least 1 argument"));
3849 }
3850 self.output.push(format!("emit: {}", args[0]));
3851 Ok(args[0].clone())
3852 }
3853 #[cfg(feature = "native")]
3854 BuiltinId::Lineage => Ok(VmValue::String(Arc::from("lineage_tracker"))),
3855 #[cfg(feature = "native")]
3856 BuiltinId::RunPipeline => {
3857 if args.is_empty() {
3858 return Err(runtime_err("run_pipeline() requires a pipeline"));
3859 }
3860 if let VmValue::PipelineDef(ref def) = args[0] {
3861 Ok(VmValue::String(Arc::from(
3862 format!("Pipeline '{}' triggered", def.name).as_str(),
3863 )))
3864 } else {
3865 Err(runtime_err("run_pipeline: argument must be a pipeline"))
3866 }
3867 }
3868 #[cfg(not(feature = "native"))]
3869 BuiltinId::AlertSlack
3870 | BuiltinId::AlertWebhook
3871 | BuiltinId::Emit
3872 | BuiltinId::Lineage
3873 | BuiltinId::RunPipeline => Err(runtime_err("Streaming not available in WASM")),
3874 BuiltinId::Sqrt => match args.first() {
3876 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sqrt())),
3877 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sqrt())),
3878 _ => Err(runtime_err("sqrt() expects a number")),
3879 },
3880 BuiltinId::Pow => {
3881 if args.len() == 2 {
3882 match (&args[0], &args[1]) {
3883 (VmValue::Float(a), VmValue::Float(b)) => Ok(VmValue::Float(a.powf(*b))),
3884 (VmValue::Int(a), VmValue::Int(b)) => {
3885 Ok(VmValue::Float((*a as f64).powf(*b as f64)))
3886 }
3887 (VmValue::Float(a), VmValue::Int(b)) => {
3888 Ok(VmValue::Float(a.powf(*b as f64)))
3889 }
3890 (VmValue::Int(a), VmValue::Float(b)) => {
3891 Ok(VmValue::Float((*a as f64).powf(*b)))
3892 }
3893 _ => Err(runtime_err("pow() expects two numbers")),
3894 }
3895 } else {
3896 Err(runtime_err("pow() expects 2 arguments"))
3897 }
3898 }
3899 BuiltinId::Floor => match args.first() {
3900 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.floor())),
3901 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3902 _ => Err(runtime_err("floor() expects a number")),
3903 },
3904 BuiltinId::Ceil => match args.first() {
3905 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ceil())),
3906 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3907 _ => Err(runtime_err("ceil() expects a number")),
3908 },
3909 BuiltinId::Round => match args.first() {
3910 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.round())),
3911 Some(VmValue::Int(n)) => Ok(VmValue::Int(*n)),
3912 _ => Err(runtime_err("round() expects a number")),
3913 },
3914 BuiltinId::Sin => match args.first() {
3915 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.sin())),
3916 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).sin())),
3917 _ => Err(runtime_err("sin() expects a number")),
3918 },
3919 BuiltinId::Cos => match args.first() {
3920 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.cos())),
3921 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).cos())),
3922 _ => Err(runtime_err("cos() expects a number")),
3923 },
3924 BuiltinId::Tan => match args.first() {
3925 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.tan())),
3926 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).tan())),
3927 _ => Err(runtime_err("tan() expects a number")),
3928 },
3929 BuiltinId::Log => match args.first() {
3930 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.ln())),
3931 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).ln())),
3932 _ => Err(runtime_err("log() expects a number")),
3933 },
3934 BuiltinId::Log2 => match args.first() {
3935 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log2())),
3936 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log2())),
3937 _ => Err(runtime_err("log2() expects a number")),
3938 },
3939 BuiltinId::Log10 => match args.first() {
3940 Some(VmValue::Float(n)) => Ok(VmValue::Float(n.log10())),
3941 Some(VmValue::Int(n)) => Ok(VmValue::Float((*n as f64).log10())),
3942 _ => Err(runtime_err("log10() expects a number")),
3943 },
3944 BuiltinId::Join => {
3945 if args.len() == 2 {
3946 if let (VmValue::String(sep), VmValue::List(items)) = (&args[0], &args[1]) {
3947 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
3948 Ok(VmValue::String(Arc::from(
3949 parts.join(sep.as_ref()).as_str(),
3950 )))
3951 } else {
3952 Err(runtime_err("join() expects separator and list"))
3953 }
3954 } else {
3955 Err(runtime_err("join() expects 2 arguments"))
3956 }
3957 }
3958 #[cfg(feature = "native")]
3959 BuiltinId::HttpGet => {
3960 self.check_permission("network")?;
3961 if args.is_empty() {
3962 return Err(runtime_err("http_get() expects a URL"));
3963 }
3964 if let VmValue::String(url) = &args[0] {
3965 match reqwest::blocking::get(url.as_ref()).and_then(|r| r.text()) {
3966 Ok(body) => Ok(VmValue::String(Arc::from(body.as_str()))),
3967 Err(e) => {
3968 let msg = format!("HTTP GET error: {e}");
3969 self.thrown_value =
3970 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
3971 type_name: Arc::from("NetworkError"),
3972 variant: Arc::from("HttpError"),
3973 fields: vec![
3974 VmValue::String(Arc::from(msg.as_str())),
3975 VmValue::String(url.clone()),
3976 ],
3977 })));
3978 Err(runtime_err(msg))
3979 }
3980 }
3981 } else {
3982 Err(runtime_err("http_get() expects a string URL"))
3983 }
3984 }
3985 #[cfg(feature = "native")]
3986 BuiltinId::HttpPost => {
3987 self.check_permission("network")?;
3988 if args.len() < 2 {
3989 return Err(runtime_err("http_post() expects URL and body"));
3990 }
3991 if let (VmValue::String(url), VmValue::String(body)) = (&args[0], &args[1]) {
3992 let client = reqwest::blocking::Client::new();
3993 match client
3994 .post(url.as_ref())
3995 .header("Content-Type", "application/json")
3996 .body(body.to_string())
3997 .send()
3998 .and_then(|r| r.text())
3999 {
4000 Ok(resp) => Ok(VmValue::String(Arc::from(resp.as_str()))),
4001 Err(e) => {
4002 let msg = format!("HTTP POST error: {e}");
4003 self.thrown_value =
4004 Some(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4005 type_name: Arc::from("NetworkError"),
4006 variant: Arc::from("HttpError"),
4007 fields: vec![
4008 VmValue::String(Arc::from(msg.as_str())),
4009 VmValue::String(url.clone()),
4010 ],
4011 })));
4012 Err(runtime_err(msg))
4013 }
4014 }
4015 } else {
4016 Err(runtime_err("http_post() expects string URL and body"))
4017 }
4018 }
4019 #[cfg(not(feature = "native"))]
4020 BuiltinId::HttpGet | BuiltinId::HttpPost => {
4021 Err(runtime_err("HTTP requests not available in WASM"))
4022 }
4023 BuiltinId::Assert => {
4024 if args.is_empty() {
4025 return Err(runtime_err("assert() expects at least 1 argument"));
4026 }
4027 if !args[0].is_truthy() {
4028 let msg = if args.len() > 1 {
4029 format!("{}", args[1])
4030 } else {
4031 "Assertion failed".to_string()
4032 };
4033 Err(runtime_err(msg))
4034 } else {
4035 Ok(VmValue::None)
4036 }
4037 }
4038 BuiltinId::AssertEq => {
4039 if args.len() < 2 {
4040 return Err(runtime_err("assert_eq() expects 2 arguments"));
4041 }
4042 let eq = match (&args[0], &args[1]) {
4043 (VmValue::Int(a), VmValue::Int(b)) => a == b,
4044 (VmValue::Float(a), VmValue::Float(b)) => a == b,
4045 (VmValue::String(a), VmValue::String(b)) => a == b,
4046 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
4047 (VmValue::None, VmValue::None) => true,
4048 _ => false,
4049 };
4050 if !eq {
4051 Err(runtime_err(format!(
4052 "Assertion failed: {} != {}",
4053 args[0], args[1]
4054 )))
4055 } else {
4056 Ok(VmValue::None)
4057 }
4058 }
4059 BuiltinId::JsonParse => {
4061 if args.is_empty() {
4062 return Err(runtime_err("json_parse() expects a string"));
4063 }
4064 if let VmValue::String(s) = &args[0] {
4065 let json_val: serde_json::Value = serde_json::from_str(s)
4066 .map_err(|e| runtime_err(format!("JSON parse error: {e}")))?;
4067 Ok(vm_json_to_value(&json_val))
4068 } else {
4069 Err(runtime_err("json_parse() expects a string"))
4070 }
4071 }
4072 BuiltinId::JsonStringify => {
4073 if args.is_empty() {
4074 return Err(runtime_err("json_stringify() expects a value"));
4075 }
4076 let json = vm_value_to_json(&args[0]);
4077 Ok(VmValue::String(Arc::from(json.to_string().as_str())))
4078 }
4079 BuiltinId::MapFrom => {
4080 if !args.len().is_multiple_of(2) {
4081 return Err(runtime_err(
4082 "map_from() expects even number of arguments (key, value pairs)",
4083 ));
4084 }
4085 let mut pairs = Vec::new();
4086 for chunk in args.chunks(2) {
4087 let key = match &chunk[0] {
4088 VmValue::String(s) => s.clone(),
4089 other => Arc::from(format!("{other}").as_str()),
4090 };
4091 pairs.push((key, chunk[1].clone()));
4092 }
4093 Ok(VmValue::Map(Box::new(pairs)))
4094 }
4095 #[cfg(feature = "native")]
4096 BuiltinId::ReadFile => {
4097 self.check_permission("file_read")?;
4098 if args.is_empty() {
4099 return Err(runtime_err("read_file() expects a path"));
4100 }
4101 if let VmValue::String(path) = &args[0] {
4102 let content = std::fs::read_to_string(path.as_ref())
4103 .map_err(|e| runtime_err(format!("read_file error: {e}")))?;
4104 Ok(VmValue::String(Arc::from(content.as_str())))
4105 } else {
4106 Err(runtime_err("read_file() expects a string path"))
4107 }
4108 }
4109 #[cfg(feature = "native")]
4110 BuiltinId::WriteFile => {
4111 self.check_permission("file_write")?;
4112 if args.len() < 2 {
4113 return Err(runtime_err("write_file() expects path and content"));
4114 }
4115 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
4116 std::fs::write(path.as_ref(), content.as_ref())
4117 .map_err(|e| runtime_err(format!("write_file error: {e}")))?;
4118 Ok(VmValue::None)
4119 } else {
4120 Err(runtime_err("write_file() expects string path and content"))
4121 }
4122 }
4123 #[cfg(feature = "native")]
4124 BuiltinId::AppendFile => {
4125 self.check_permission("file_write")?;
4126 if args.len() < 2 {
4127 return Err(runtime_err("append_file() expects path and content"));
4128 }
4129 if let (VmValue::String(path), VmValue::String(content)) = (&args[0], &args[1]) {
4130 use std::io::Write;
4131 let mut file = std::fs::OpenOptions::new()
4132 .create(true)
4133 .append(true)
4134 .open(path.as_ref())
4135 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
4136 file.write_all(content.as_bytes())
4137 .map_err(|e| runtime_err(format!("append_file error: {e}")))?;
4138 Ok(VmValue::None)
4139 } else {
4140 Err(runtime_err("append_file() expects string path and content"))
4141 }
4142 }
4143 #[cfg(feature = "native")]
4144 BuiltinId::FileExists => {
4145 self.check_permission("file_read")?;
4146 if args.is_empty() {
4147 return Err(runtime_err("file_exists() expects a path"));
4148 }
4149 if let VmValue::String(path) = &args[0] {
4150 Ok(VmValue::Bool(std::path::Path::new(path.as_ref()).exists()))
4151 } else {
4152 Err(runtime_err("file_exists() expects a string path"))
4153 }
4154 }
4155 #[cfg(feature = "native")]
4156 BuiltinId::ListDir => {
4157 self.check_permission("file_read")?;
4158 if args.is_empty() {
4159 return Err(runtime_err("list_dir() expects a path"));
4160 }
4161 if let VmValue::String(path) = &args[0] {
4162 let entries: Vec<VmValue> = std::fs::read_dir(path.as_ref())
4163 .map_err(|e| runtime_err(format!("list_dir error: {e}")))?
4164 .filter_map(|e| e.ok())
4165 .map(|e| {
4166 VmValue::String(Arc::from(e.file_name().to_string_lossy().as_ref()))
4167 })
4168 .collect();
4169 Ok(VmValue::List(Box::new(entries)))
4170 } else {
4171 Err(runtime_err("list_dir() expects a string path"))
4172 }
4173 }
4174 #[cfg(not(feature = "native"))]
4175 BuiltinId::ReadFile
4176 | BuiltinId::WriteFile
4177 | BuiltinId::AppendFile
4178 | BuiltinId::FileExists
4179 | BuiltinId::ListDir => Err(runtime_err("File I/O not available in WASM")),
4180 #[cfg(feature = "native")]
4181 BuiltinId::EnvGet => {
4182 if args.is_empty() {
4183 return Err(runtime_err("env_get() expects a name"));
4184 }
4185 if let VmValue::String(name) = &args[0] {
4186 match std::env::var(name.as_ref()) {
4187 Ok(val) => Ok(VmValue::String(Arc::from(val.as_str()))),
4188 Err(_) => Ok(VmValue::None),
4189 }
4190 } else {
4191 Err(runtime_err("env_get() expects a string"))
4192 }
4193 }
4194 #[cfg(feature = "native")]
4195 BuiltinId::EnvSet => {
4196 self.check_permission("env_write")?;
4197 if args.len() < 2 {
4198 return Err(runtime_err("env_set() expects name and value"));
4199 }
4200 if let (VmValue::String(name), VmValue::String(val)) = (&args[0], &args[1]) {
4201 let _guard = env_lock();
4202 unsafe {
4203 std::env::set_var(name.as_ref(), val.as_ref());
4204 }
4205 Ok(VmValue::None)
4206 } else {
4207 Err(runtime_err("env_set() expects two strings"))
4208 }
4209 }
4210 #[cfg(not(feature = "native"))]
4211 BuiltinId::EnvGet | BuiltinId::EnvSet => {
4212 Err(runtime_err("Environment variables not available in WASM"))
4213 }
4214 BuiltinId::RegexMatch => {
4215 if args.len() < 2 {
4216 return Err(runtime_err("regex_match() expects pattern and string"));
4217 }
4218 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
4219 if pattern.len() > 10_000 {
4220 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4221 }
4222 let re = regex::RegexBuilder::new(pattern)
4223 .size_limit(10_000_000)
4224 .build()
4225 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4226 Ok(VmValue::Bool(re.is_match(text)))
4227 } else {
4228 Err(runtime_err(
4229 "regex_match() expects string pattern and string",
4230 ))
4231 }
4232 }
4233 BuiltinId::RegexFind => {
4234 if args.len() < 2 {
4235 return Err(runtime_err("regex_find() expects pattern and string"));
4236 }
4237 if let (VmValue::String(pattern), VmValue::String(text)) = (&args[0], &args[1]) {
4238 if pattern.len() > 10_000 {
4239 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4240 }
4241 let re = regex::RegexBuilder::new(pattern)
4242 .size_limit(10_000_000)
4243 .build()
4244 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4245 let matches: Vec<VmValue> = re
4246 .find_iter(text)
4247 .map(|m| VmValue::String(Arc::from(m.as_str())))
4248 .collect();
4249 Ok(VmValue::List(Box::new(matches)))
4250 } else {
4251 Err(runtime_err(
4252 "regex_find() expects string pattern and string",
4253 ))
4254 }
4255 }
4256 BuiltinId::RegexReplace => {
4257 if args.len() < 3 {
4258 return Err(runtime_err(
4259 "regex_replace() expects pattern, string, replacement",
4260 ));
4261 }
4262 if let (
4263 VmValue::String(pattern),
4264 VmValue::String(text),
4265 VmValue::String(replacement),
4266 ) = (&args[0], &args[1], &args[2])
4267 {
4268 if pattern.len() > 10_000 {
4269 return Err(runtime_err("Regex pattern too large (max 10,000 chars)"));
4270 }
4271 let re = regex::RegexBuilder::new(pattern)
4272 .size_limit(10_000_000)
4273 .build()
4274 .map_err(|e| runtime_err(format!("Invalid regex: {e}")))?;
4275 Ok(VmValue::String(Arc::from(
4276 re.replace_all(text, replacement.as_ref()).as_ref(),
4277 )))
4278 } else {
4279 Err(runtime_err("regex_replace() expects three strings"))
4280 }
4281 }
4282 BuiltinId::Now => {
4283 let ts = chrono::Utc::now().timestamp_millis();
4284 Ok(VmValue::DateTime(ts))
4285 }
4286 BuiltinId::DateFormat => {
4287 if args.len() < 2 {
4288 return Err(runtime_err(
4289 "date_format() expects datetime/timestamp and format",
4290 ));
4291 }
4292 let ts = match &args[0] {
4293 VmValue::DateTime(ms) => *ms,
4294 VmValue::Int(ms) => *ms,
4295 _ => {
4296 return Err(runtime_err(
4297 "date_format() expects a datetime or int timestamp",
4298 ));
4299 }
4300 };
4301 let fmt = match &args[1] {
4302 VmValue::String(s) => s,
4303 _ => return Err(runtime_err("date_format() expects a string format")),
4304 };
4305 use chrono::TimeZone;
4306 let secs = ts / 1000;
4307 let nsecs = ((ts % 1000) * 1_000_000) as u32;
4308 let dt = chrono::Utc
4309 .timestamp_opt(secs, nsecs)
4310 .single()
4311 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
4312 Ok(VmValue::String(Arc::from(
4313 dt.format(fmt.as_ref()).to_string().as_str(),
4314 )))
4315 }
4316 BuiltinId::DateParse => {
4317 if args.len() < 2 {
4318 return Err(runtime_err("date_parse() expects string and format"));
4319 }
4320 if let (VmValue::String(s), VmValue::String(fmt)) = (&args[0], &args[1]) {
4321 let dt = chrono::NaiveDateTime::parse_from_str(s, fmt)
4322 .map_err(|e| runtime_err(format!("date_parse error: {e}")))?;
4323 let ts = dt.and_utc().timestamp_millis();
4324 Ok(VmValue::DateTime(ts))
4325 } else {
4326 Err(runtime_err("date_parse() expects two strings"))
4327 }
4328 }
4329 BuiltinId::Zip => {
4330 if args.len() < 2 {
4331 return Err(runtime_err("zip() expects two lists"));
4332 }
4333 if let (VmValue::List(a), VmValue::List(b)) = (&args[0], &args[1]) {
4334 let pairs: Vec<VmValue> = a
4335 .iter()
4336 .zip(b.iter())
4337 .map(|(x, y)| VmValue::List(Box::new(vec![x.clone(), y.clone()])))
4338 .collect();
4339 Ok(VmValue::List(Box::new(pairs)))
4340 } else {
4341 Err(runtime_err("zip() expects two lists"))
4342 }
4343 }
4344 BuiltinId::Enumerate => {
4345 if args.is_empty() {
4346 return Err(runtime_err("enumerate() expects a list"));
4347 }
4348 if let VmValue::List(items) = &args[0] {
4349 let pairs: Vec<VmValue> = items
4350 .iter()
4351 .enumerate()
4352 .map(|(i, v)| {
4353 VmValue::List(Box::new(vec![VmValue::Int(i as i64), v.clone()]))
4354 })
4355 .collect();
4356 Ok(VmValue::List(Box::new(pairs)))
4357 } else {
4358 Err(runtime_err("enumerate() expects a list"))
4359 }
4360 }
4361 BuiltinId::Bool => {
4362 if args.is_empty() {
4363 return Err(runtime_err("bool() expects a value"));
4364 }
4365 Ok(VmValue::Bool(args[0].is_truthy()))
4366 }
4367
4368 #[cfg(feature = "native")]
4370 BuiltinId::Spawn => {
4371 if args.is_empty() {
4372 return Err(runtime_err("spawn() expects a function argument"));
4373 }
4374 match &args[0] {
4375 VmValue::Function(closure) => {
4376 let proto = closure.prototype.clone();
4377 let mut closed_upvalues = Vec::new();
4379 for uv in &closure.upvalues {
4380 match uv {
4381 UpvalueRef::Open { stack_index } => {
4382 let val = self.stack[*stack_index].clone();
4383 closed_upvalues.push(UpvalueRef::Closed(val));
4384 }
4385 UpvalueRef::Closed(v) => {
4386 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4387 }
4388 }
4389 }
4390 let globals = self.globals.clone();
4391 let (tx, rx) = mpsc::channel::<Result<VmValue, String>>();
4392
4393 std::thread::spawn(move || {
4394 let mut vm = Vm::new();
4395 vm.globals = globals;
4396 let result = vm.execute_closure(&proto, &closed_upvalues);
4397 let _ = tx.send(result.map_err(|e| match e {
4398 TlError::Runtime(re) => re.message,
4399 other => format!("{other}"),
4400 }));
4401 });
4402
4403 Ok(VmValue::Task(Arc::new(VmTask::new(rx))))
4404 }
4405 _ => Err(runtime_err("spawn() expects a function")),
4406 }
4407 }
4408 #[cfg(feature = "native")]
4409 BuiltinId::Sleep => {
4410 if args.is_empty() {
4411 return Err(runtime_err("sleep() expects a duration in milliseconds"));
4412 }
4413 match &args[0] {
4414 VmValue::Int(ms) => {
4415 std::thread::sleep(Duration::from_millis(*ms as u64));
4416 Ok(VmValue::None)
4417 }
4418 _ => Err(runtime_err("sleep() expects an integer (milliseconds)")),
4419 }
4420 }
4421 #[cfg(feature = "native")]
4422 BuiltinId::Channel => {
4423 let capacity = match args.first() {
4424 Some(VmValue::Int(n)) => *n as usize,
4425 None => 64,
4426 _ => {
4427 return Err(runtime_err(
4428 "channel() expects an optional integer capacity",
4429 ));
4430 }
4431 };
4432 Ok(VmValue::Channel(Arc::new(VmChannel::new(capacity))))
4433 }
4434 #[cfg(feature = "native")]
4435 BuiltinId::Send => {
4436 if args.len() < 2 {
4437 return Err(runtime_err("send() expects a channel and a value"));
4438 }
4439 match &args[0] {
4440 VmValue::Channel(ch) => {
4441 ch.sender
4442 .send(args[1].clone())
4443 .map_err(|_| runtime_err("Channel disconnected"))?;
4444 Ok(VmValue::None)
4445 }
4446 _ => Err(runtime_err("send() expects a channel as first argument")),
4447 }
4448 }
4449 #[cfg(feature = "native")]
4450 BuiltinId::Recv => {
4451 if args.is_empty() {
4452 return Err(runtime_err("recv() expects a channel"));
4453 }
4454 match &args[0] {
4455 VmValue::Channel(ch) => {
4456 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4457 match guard.recv() {
4458 Ok(val) => Ok(val),
4459 Err(_) => Ok(VmValue::None),
4460 }
4461 }
4462 _ => Err(runtime_err("recv() expects a channel")),
4463 }
4464 }
4465 #[cfg(feature = "native")]
4466 BuiltinId::TryRecv => {
4467 if args.is_empty() {
4468 return Err(runtime_err("try_recv() expects a channel"));
4469 }
4470 match &args[0] {
4471 VmValue::Channel(ch) => {
4472 let guard = ch.receiver.lock().unwrap_or_else(|e| e.into_inner());
4473 match guard.try_recv() {
4474 Ok(val) => Ok(val),
4475 Err(_) => Ok(VmValue::None),
4476 }
4477 }
4478 _ => Err(runtime_err("try_recv() expects a channel")),
4479 }
4480 }
4481 #[cfg(feature = "native")]
4482 BuiltinId::AwaitAll => {
4483 if args.is_empty() {
4484 return Err(runtime_err("await_all() expects a list of tasks"));
4485 }
4486 match &args[0] {
4487 VmValue::List(tasks) => {
4488 let mut results = Vec::with_capacity(tasks.len());
4489 for task in tasks.iter() {
4490 match task {
4491 VmValue::Task(t) => {
4492 let rx = {
4493 let mut guard =
4494 t.receiver.lock().unwrap_or_else(|e| e.into_inner());
4495 guard.take()
4496 };
4497 match rx {
4498 Some(receiver) => match receiver.recv() {
4499 Ok(Ok(val)) => results.push(val),
4500 Ok(Err(e)) => return Err(runtime_err(e)),
4501 Err(_) => {
4502 return Err(runtime_err(
4503 "Task channel disconnected",
4504 ));
4505 }
4506 },
4507 None => return Err(runtime_err("Task already awaited")),
4508 }
4509 }
4510 other => results.push(other.clone()),
4511 }
4512 }
4513 Ok(VmValue::List(Box::new(results)))
4514 }
4515 _ => Err(runtime_err("await_all() expects a list")),
4516 }
4517 }
4518 #[cfg(feature = "native")]
4519 BuiltinId::Pmap => {
4520 if args.len() < 2 {
4521 return Err(runtime_err("pmap() expects a list and a function"));
4522 }
4523 let items = match &args[0] {
4524 VmValue::List(items) => (**items).clone(),
4525 _ => return Err(runtime_err("pmap() expects a list as first argument")),
4526 };
4527 let closure = match &args[1] {
4528 VmValue::Function(c) => c.clone(),
4529 _ => return Err(runtime_err("pmap() expects a function as second argument")),
4530 };
4531
4532 let mut closed_upvalues = Vec::new();
4534 for uv in &closure.upvalues {
4535 match uv {
4536 UpvalueRef::Open { stack_index } => {
4537 let val = self.stack[*stack_index].clone();
4538 closed_upvalues.push(UpvalueRef::Closed(val));
4539 }
4540 UpvalueRef::Closed(v) => {
4541 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
4542 }
4543 }
4544 }
4545
4546 let proto = closure.prototype.clone();
4547 let globals = self.globals.clone();
4548
4549 let mut handles = Vec::with_capacity(items.len());
4551 for item in items {
4552 let proto = proto.clone();
4553 let upvalues = closed_upvalues.clone();
4554 let globals = globals.clone();
4555 let handle = std::thread::spawn(move || {
4556 let mut vm = Vm::new();
4557 vm.globals = globals;
4558 vm.execute_closure_with_args(&proto, &upvalues, &[item])
4559 .map_err(|e| match e {
4560 TlError::Runtime(re) => re.message,
4561 other => format!("{other}"),
4562 })
4563 });
4564 handles.push(handle);
4565 }
4566
4567 let mut results = Vec::with_capacity(handles.len());
4568 for handle in handles {
4569 match handle.join() {
4570 Ok(Ok(val)) => results.push(val),
4571 Ok(Err(e)) => return Err(runtime_err(e)),
4572 Err(_) => return Err(runtime_err("pmap() thread panicked")),
4573 }
4574 }
4575 Ok(VmValue::List(Box::new(results)))
4576 }
4577 #[cfg(feature = "native")]
4578 BuiltinId::Timeout => {
4579 if args.len() < 2 {
4580 return Err(runtime_err(
4581 "timeout() expects a task and a duration in milliseconds",
4582 ));
4583 }
4584 let ms = match &args[1] {
4585 VmValue::Int(n) => *n as u64,
4586 _ => return Err(runtime_err("timeout() expects an integer duration")),
4587 };
4588 match &args[0] {
4589 VmValue::Task(task) => {
4590 let rx = {
4591 let mut guard = task.receiver.lock().unwrap_or_else(|e| e.into_inner());
4592 guard.take()
4593 };
4594 match rx {
4595 Some(receiver) => {
4596 match receiver.recv_timeout(Duration::from_millis(ms)) {
4597 Ok(Ok(val)) => Ok(val),
4598 Ok(Err(e)) => Err(runtime_err(e)),
4599 Err(mpsc::RecvTimeoutError::Timeout) => {
4600 Err(runtime_err("Task timed out"))
4601 }
4602 Err(mpsc::RecvTimeoutError::Disconnected) => {
4603 Err(runtime_err("Task channel disconnected"))
4604 }
4605 }
4606 }
4607 None => Err(runtime_err("Task already awaited")),
4608 }
4609 }
4610 _ => Err(runtime_err("timeout() expects a task as first argument")),
4611 }
4612 }
4613 #[cfg(not(feature = "native"))]
4614 BuiltinId::Spawn
4615 | BuiltinId::Sleep
4616 | BuiltinId::Channel
4617 | BuiltinId::Send
4618 | BuiltinId::Recv
4619 | BuiltinId::TryRecv
4620 | BuiltinId::AwaitAll
4621 | BuiltinId::Pmap
4622 | BuiltinId::Timeout => Err(runtime_err("Threading not available in WASM")),
4623 BuiltinId::Next => {
4625 if args.is_empty() {
4626 return Err(runtime_err("next() expects a generator"));
4627 }
4628 match &args[0] {
4629 VmValue::Generator(gen_arc) => {
4630 let g = gen_arc.clone();
4631 self.generator_next(&g)
4632 }
4633 _ => Err(runtime_err("next() expects a generator")),
4634 }
4635 }
4636 BuiltinId::IsGenerator => {
4637 let val = args.first().unwrap_or(&VmValue::None);
4638 Ok(VmValue::Bool(matches!(val, VmValue::Generator(_))))
4639 }
4640 BuiltinId::Iter => {
4641 if args.is_empty() {
4642 return Err(runtime_err("iter() expects a list"));
4643 }
4644 match &args[0] {
4645 VmValue::List(items) => {
4646 let gn = VmGenerator::new(GeneratorKind::ListIter {
4647 items: (**items).clone(),
4648 index: 0,
4649 });
4650 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4651 }
4652 _ => Err(runtime_err("iter() expects a list")),
4653 }
4654 }
4655 BuiltinId::Take => {
4656 if args.len() < 2 {
4657 return Err(runtime_err("take() expects a generator and a count"));
4658 }
4659 let gen_arc = match &args[0] {
4660 VmValue::Generator(g) => g.clone(),
4661 _ => return Err(runtime_err("take() expects a generator as first argument")),
4662 };
4663 let n = match &args[1] {
4664 VmValue::Int(n) => *n as usize,
4665 _ => return Err(runtime_err("take() expects an integer count")),
4666 };
4667 let gn = VmGenerator::new(GeneratorKind::Take {
4668 source: gen_arc,
4669 remaining: n,
4670 });
4671 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4672 }
4673 BuiltinId::Skip_ => {
4674 if args.len() < 2 {
4675 return Err(runtime_err("skip() expects a generator and a count"));
4676 }
4677 let gen_arc = match &args[0] {
4678 VmValue::Generator(g) => g.clone(),
4679 _ => return Err(runtime_err("skip() expects a generator as first argument")),
4680 };
4681 let n = match &args[1] {
4682 VmValue::Int(n) => *n as usize,
4683 _ => return Err(runtime_err("skip() expects an integer count")),
4684 };
4685 let gn = VmGenerator::new(GeneratorKind::Skip {
4686 source: gen_arc,
4687 remaining: n,
4688 });
4689 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4690 }
4691 BuiltinId::GenCollect => {
4692 if args.is_empty() {
4693 return Err(runtime_err("gen_collect() expects a generator"));
4694 }
4695 match &args[0] {
4696 VmValue::Generator(gen_arc) => {
4697 let g = gen_arc.clone();
4698 let mut items = Vec::new();
4699 loop {
4700 let val = self.generator_next(&g)?;
4701 if matches!(val, VmValue::None) {
4702 break;
4703 }
4704 items.push(val);
4705 }
4706 Ok(VmValue::List(Box::new(items)))
4707 }
4708 _ => Err(runtime_err("gen_collect() expects a generator")),
4709 }
4710 }
4711 BuiltinId::GenMap => {
4712 if args.len() < 2 {
4713 return Err(runtime_err("gen_map() expects a generator and a function"));
4714 }
4715 let gen_arc = match &args[0] {
4716 VmValue::Generator(g) => g.clone(),
4717 _ => {
4718 return Err(runtime_err(
4719 "gen_map() expects a generator as first argument",
4720 ));
4721 }
4722 };
4723 let func = args[1].clone();
4724 let gn = VmGenerator::new(GeneratorKind::Map {
4725 source: gen_arc,
4726 func,
4727 });
4728 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4729 }
4730 BuiltinId::GenFilter => {
4731 if args.len() < 2 {
4732 return Err(runtime_err(
4733 "gen_filter() expects a generator and a function",
4734 ));
4735 }
4736 let gen_arc = match &args[0] {
4737 VmValue::Generator(g) => g.clone(),
4738 _ => {
4739 return Err(runtime_err(
4740 "gen_filter() expects a generator as first argument",
4741 ));
4742 }
4743 };
4744 let func = args[1].clone();
4745 let gn = VmGenerator::new(GeneratorKind::Filter {
4746 source: gen_arc,
4747 func,
4748 });
4749 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4750 }
4751 BuiltinId::Chain => {
4752 if args.len() < 2 {
4753 return Err(runtime_err("chain() expects two generators"));
4754 }
4755 let first = match &args[0] {
4756 VmValue::Generator(g) => g.clone(),
4757 _ => return Err(runtime_err("chain() expects generators")),
4758 };
4759 let second = match &args[1] {
4760 VmValue::Generator(g) => g.clone(),
4761 _ => return Err(runtime_err("chain() expects generators")),
4762 };
4763 let gn = VmGenerator::new(GeneratorKind::Chain {
4764 first,
4765 second,
4766 on_second: false,
4767 });
4768 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4769 }
4770 BuiltinId::GenZip => {
4771 if args.len() < 2 {
4772 return Err(runtime_err("gen_zip() expects two generators"));
4773 }
4774 let first = match &args[0] {
4775 VmValue::Generator(g) => g.clone(),
4776 _ => return Err(runtime_err("gen_zip() expects generators")),
4777 };
4778 let second = match &args[1] {
4779 VmValue::Generator(g) => g.clone(),
4780 _ => return Err(runtime_err("gen_zip() expects generators")),
4781 };
4782 let gn = VmGenerator::new(GeneratorKind::Zip { first, second });
4783 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4784 }
4785 BuiltinId::GenEnumerate => {
4786 if args.is_empty() {
4787 return Err(runtime_err("gen_enumerate() expects a generator"));
4788 }
4789 let gen_arc = match &args[0] {
4790 VmValue::Generator(g) => g.clone(),
4791 _ => return Err(runtime_err("gen_enumerate() expects a generator")),
4792 };
4793 let gn = VmGenerator::new(GeneratorKind::Enumerate {
4794 source: gen_arc,
4795 index: 0,
4796 });
4797 Ok(VmValue::Generator(Arc::new(Mutex::new(gn))))
4798 }
4799 BuiltinId::Ok => {
4801 let val = if args.is_empty() {
4802 VmValue::None
4803 } else {
4804 args[0].clone()
4805 };
4806 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4807 type_name: Arc::from("Result"),
4808 variant: Arc::from("Ok"),
4809 fields: vec![val],
4810 })))
4811 }
4812 BuiltinId::Err_ => {
4813 let val = if args.is_empty() {
4814 VmValue::String(Arc::from("error"))
4815 } else {
4816 args[0].clone()
4817 };
4818 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
4819 type_name: Arc::from("Result"),
4820 variant: Arc::from("Err"),
4821 fields: vec![val],
4822 })))
4823 }
4824 BuiltinId::IsOk => {
4825 if args.is_empty() {
4826 return Err(runtime_err("is_ok() expects an argument"));
4827 }
4828 match &args[0] {
4829 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4830 Ok(VmValue::Bool(ei.variant.as_ref() == "Ok"))
4831 }
4832 _ => Ok(VmValue::Bool(false)),
4833 }
4834 }
4835 BuiltinId::IsErr => {
4836 if args.is_empty() {
4837 return Err(runtime_err("is_err() expects an argument"));
4838 }
4839 match &args[0] {
4840 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4841 Ok(VmValue::Bool(ei.variant.as_ref() == "Err"))
4842 }
4843 _ => Ok(VmValue::Bool(false)),
4844 }
4845 }
4846 BuiltinId::Unwrap => {
4847 if args.is_empty() {
4848 return Err(runtime_err("unwrap() expects an argument"));
4849 }
4850 match &args[0] {
4851 VmValue::EnumInstance(ei) if ei.type_name.as_ref() == "Result" => {
4852 if ei.variant.as_ref() == "Ok" && !ei.fields.is_empty() {
4853 Ok(ei.fields[0].clone())
4854 } else if ei.variant.as_ref() == "Err" {
4855 let msg = if ei.fields.is_empty() {
4856 "error".to_string()
4857 } else {
4858 format!("{}", ei.fields[0])
4859 };
4860 Err(runtime_err(format!("unwrap() called on Err({msg})")))
4861 } else {
4862 Ok(VmValue::None)
4863 }
4864 }
4865 VmValue::None => Err(runtime_err("unwrap() called on none".to_string())),
4866 other => Ok(other.clone()),
4867 }
4868 }
4869 BuiltinId::SetFrom => {
4870 let list = match args.first() {
4871 Some(VmValue::List(items)) => items,
4872 _ => return Err(runtime_err("set_from() expects a list")),
4873 };
4874 if list.is_empty() {
4875 return Ok(VmValue::Set(Box::default()));
4876 }
4877 let mut result = Vec::new();
4878 for item in list.iter() {
4879 if !result.iter().any(|x| vm_values_equal(x, item)) {
4880 result.push(item.clone());
4881 }
4882 }
4883 Ok(VmValue::Set(Box::new(result)))
4884 }
4885 BuiltinId::SetAdd => {
4886 if args.len() < 2 {
4887 return Err(runtime_err("set_add() expects 2 arguments"));
4888 }
4889 let val = &args[1];
4890 match &args[0] {
4891 VmValue::Set(items) => {
4892 let mut new_items = items.clone();
4893 if !new_items.iter().any(|x| vm_values_equal(x, val)) {
4894 new_items.push(val.clone());
4895 }
4896 Ok(VmValue::Set(new_items))
4897 }
4898 _ => Err(runtime_err("set_add() first argument must be a set")),
4899 }
4900 }
4901 BuiltinId::SetRemove => {
4902 if args.len() < 2 {
4903 return Err(runtime_err("set_remove() expects 2 arguments"));
4904 }
4905 let val = &args[1];
4906 match &args[0] {
4907 VmValue::Set(items) => {
4908 let new_items: Vec<VmValue> = items
4909 .iter()
4910 .filter(|x| !vm_values_equal(x, val))
4911 .cloned()
4912 .collect();
4913 Ok(VmValue::Set(Box::new(new_items)))
4914 }
4915 _ => Err(runtime_err("set_remove() first argument must be a set")),
4916 }
4917 }
4918 BuiltinId::SetContains => {
4919 if args.len() < 2 {
4920 return Err(runtime_err("set_contains() expects 2 arguments"));
4921 }
4922 let val = &args[1];
4923 match &args[0] {
4924 VmValue::Set(items) => {
4925 Ok(VmValue::Bool(items.iter().any(|x| vm_values_equal(x, val))))
4926 }
4927 _ => Err(runtime_err("set_contains() first argument must be a set")),
4928 }
4929 }
4930 BuiltinId::SetUnion => {
4931 if args.len() < 2 {
4932 return Err(runtime_err("set_union() expects 2 arguments"));
4933 }
4934 match (&args[0], &args[1]) {
4935 (VmValue::Set(a), VmValue::Set(b)) => {
4936 let mut result = a.clone();
4937 for item in b.iter() {
4938 if !result.iter().any(|x| vm_values_equal(x, item)) {
4939 result.push(item.clone());
4940 }
4941 }
4942 Ok(VmValue::Set(result))
4943 }
4944 _ => Err(runtime_err("set_union() expects two sets")),
4945 }
4946 }
4947 BuiltinId::SetIntersection => {
4948 if args.len() < 2 {
4949 return Err(runtime_err("set_intersection() expects 2 arguments"));
4950 }
4951 match (&args[0], &args[1]) {
4952 (VmValue::Set(a), VmValue::Set(b)) => {
4953 let result: Vec<VmValue> = a
4954 .iter()
4955 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
4956 .cloned()
4957 .collect();
4958 Ok(VmValue::Set(Box::new(result)))
4959 }
4960 _ => Err(runtime_err("set_intersection() expects two sets")),
4961 }
4962 }
4963 BuiltinId::SetDifference => {
4964 if args.len() < 2 {
4965 return Err(runtime_err("set_difference() expects 2 arguments"));
4966 }
4967 match (&args[0], &args[1]) {
4968 (VmValue::Set(a), VmValue::Set(b)) => {
4969 let result: Vec<VmValue> = a
4970 .iter()
4971 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
4972 .cloned()
4973 .collect();
4974 Ok(VmValue::Set(Box::new(result)))
4975 }
4976 _ => Err(runtime_err("set_difference() expects two sets")),
4977 }
4978 }
4979
4980 #[cfg(feature = "native")]
4982 BuiltinId::FillNull => {
4983 if args.len() < 2 {
4984 return Err(runtime_err(
4985 "fill_null() expects (table, column, [strategy], [value])",
4986 ));
4987 }
4988 let df = match &args[0] {
4989 VmValue::Table(t) => t.df.clone(),
4990 _ => return Err(runtime_err("fill_null() first arg must be a table")),
4991 };
4992 let column = match &args[1] {
4993 VmValue::String(s) => s.to_string(),
4994 _ => return Err(runtime_err("fill_null() column must be a string")),
4995 };
4996 let strategy = if args.len() > 2 {
4997 match &args[2] {
4998 VmValue::String(s) => s.to_string(),
4999 _ => "value".to_string(),
5000 }
5001 } else {
5002 "value".to_string()
5003 };
5004 let fill_value = if args.len() > 3 {
5005 match &args[3] {
5006 VmValue::Int(n) => Some(*n as f64),
5007 VmValue::Float(f) => Some(*f),
5008 _ => None,
5009 }
5010 } else if args.len() > 2 && strategy == "value" {
5011 match &args[2] {
5012 VmValue::Int(n) => {
5013 return Ok(VmValue::Table(VmTable {
5014 df: self
5015 .engine()
5016 .fill_null(df, &column, "value", Some(*n as f64))
5017 .map_err(runtime_err)?,
5018 }));
5019 }
5020 VmValue::Float(f) => {
5021 return Ok(VmValue::Table(VmTable {
5022 df: self
5023 .engine()
5024 .fill_null(df, &column, "value", Some(*f))
5025 .map_err(runtime_err)?,
5026 }));
5027 }
5028 _ => None,
5029 }
5030 } else {
5031 None
5032 };
5033 let result = self
5034 .engine()
5035 .fill_null(df, &column, &strategy, fill_value)
5036 .map_err(runtime_err)?;
5037 Ok(VmValue::Table(VmTable { df: result }))
5038 }
5039 #[cfg(feature = "native")]
5040 BuiltinId::DropNull => {
5041 if args.len() < 2 {
5042 return Err(runtime_err("drop_null() expects (table, column)"));
5043 }
5044 let df = match &args[0] {
5045 VmValue::Table(t) => t.df.clone(),
5046 _ => return Err(runtime_err("drop_null() first arg must be a table")),
5047 };
5048 let column = match &args[1] {
5049 VmValue::String(s) => s.to_string(),
5050 _ => return Err(runtime_err("drop_null() column must be a string")),
5051 };
5052 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
5053 Ok(VmValue::Table(VmTable { df: result }))
5054 }
5055 #[cfg(feature = "native")]
5056 BuiltinId::Dedup => {
5057 if args.is_empty() {
5058 return Err(runtime_err("dedup() expects (table, [columns...])"));
5059 }
5060 let df = match &args[0] {
5061 VmValue::Table(t) => t.df.clone(),
5062 _ => return Err(runtime_err("dedup() first arg must be a table")),
5063 };
5064 let columns: Vec<String> = args[1..]
5065 .iter()
5066 .filter_map(|a| {
5067 if let VmValue::String(s) = a {
5068 Some(s.to_string())
5069 } else {
5070 None
5071 }
5072 })
5073 .collect();
5074 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
5075 Ok(VmValue::Table(VmTable { df: result }))
5076 }
5077 #[cfg(feature = "native")]
5078 BuiltinId::Clamp => {
5079 if args.len() < 4 {
5080 return Err(runtime_err("clamp() expects (table, column, min, max)"));
5081 }
5082 let df = match &args[0] {
5083 VmValue::Table(t) => t.df.clone(),
5084 _ => return Err(runtime_err("clamp() first arg must be a table")),
5085 };
5086 let column = match &args[1] {
5087 VmValue::String(s) => s.to_string(),
5088 _ => return Err(runtime_err("clamp() column must be a string")),
5089 };
5090 let min_val = match &args[2] {
5091 VmValue::Int(n) => *n as f64,
5092 VmValue::Float(f) => *f,
5093 _ => return Err(runtime_err("clamp() min must be a number")),
5094 };
5095 let max_val = match &args[3] {
5096 VmValue::Int(n) => *n as f64,
5097 VmValue::Float(f) => *f,
5098 _ => return Err(runtime_err("clamp() max must be a number")),
5099 };
5100 let result = self
5101 .engine()
5102 .clamp(df, &column, min_val, max_val)
5103 .map_err(runtime_err)?;
5104 Ok(VmValue::Table(VmTable { df: result }))
5105 }
5106 #[cfg(feature = "native")]
5107 BuiltinId::DataProfile => {
5108 if args.is_empty() {
5109 return Err(runtime_err("data_profile() expects (table)"));
5110 }
5111 let df = match &args[0] {
5112 VmValue::Table(t) => t.df.clone(),
5113 _ => return Err(runtime_err("data_profile() arg must be a table")),
5114 };
5115 let result = self.engine().data_profile(df).map_err(runtime_err)?;
5116 Ok(VmValue::Table(VmTable { df: result }))
5117 }
5118 #[cfg(feature = "native")]
5119 BuiltinId::RowCount => {
5120 if args.is_empty() {
5121 return Err(runtime_err("row_count() expects (table)"));
5122 }
5123 let df = match &args[0] {
5124 VmValue::Table(t) => t.df.clone(),
5125 _ => return Err(runtime_err("row_count() arg must be a table")),
5126 };
5127 let count = self.engine().row_count(df).map_err(runtime_err)?;
5128 Ok(VmValue::Int(count))
5129 }
5130 #[cfg(feature = "native")]
5131 BuiltinId::NullRate => {
5132 if args.len() < 2 {
5133 return Err(runtime_err("null_rate() expects (table, column)"));
5134 }
5135 let df = match &args[0] {
5136 VmValue::Table(t) => t.df.clone(),
5137 _ => return Err(runtime_err("null_rate() first arg must be a table")),
5138 };
5139 let column = match &args[1] {
5140 VmValue::String(s) => s.to_string(),
5141 _ => return Err(runtime_err("null_rate() column must be a string")),
5142 };
5143 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
5144 Ok(VmValue::Float(rate))
5145 }
5146 #[cfg(feature = "native")]
5147 BuiltinId::IsUnique => {
5148 if args.len() < 2 {
5149 return Err(runtime_err("is_unique() expects (table, column)"));
5150 }
5151 let df = match &args[0] {
5152 VmValue::Table(t) => t.df.clone(),
5153 _ => return Err(runtime_err("is_unique() first arg must be a table")),
5154 };
5155 let column = match &args[1] {
5156 VmValue::String(s) => s.to_string(),
5157 _ => return Err(runtime_err("is_unique() column must be a string")),
5158 };
5159 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
5160 Ok(VmValue::Bool(unique))
5161 }
5162 #[cfg(not(feature = "native"))]
5163 BuiltinId::FillNull
5164 | BuiltinId::DropNull
5165 | BuiltinId::Dedup
5166 | BuiltinId::Clamp
5167 | BuiltinId::DataProfile
5168 | BuiltinId::RowCount
5169 | BuiltinId::NullRate
5170 | BuiltinId::IsUnique => Err(runtime_err("Data operations not available in WASM")),
5171 #[cfg(feature = "native")]
5172 BuiltinId::IsEmail => {
5173 if args.is_empty() {
5174 return Err(runtime_err("is_email() expects 1 argument"));
5175 }
5176 let s = match &args[0] {
5177 VmValue::String(s) => s.to_string(),
5178 _ => return Err(runtime_err("is_email() arg must be a string")),
5179 };
5180 Ok(VmValue::Bool(tl_data::validate::is_email(&s)))
5181 }
5182 #[cfg(feature = "native")]
5183 BuiltinId::IsUrl => {
5184 if args.is_empty() {
5185 return Err(runtime_err("is_url() expects 1 argument"));
5186 }
5187 let s = match &args[0] {
5188 VmValue::String(s) => s.to_string(),
5189 _ => return Err(runtime_err("is_url() arg must be a string")),
5190 };
5191 Ok(VmValue::Bool(tl_data::validate::is_url(&s)))
5192 }
5193 #[cfg(feature = "native")]
5194 BuiltinId::IsPhone => {
5195 if args.is_empty() {
5196 return Err(runtime_err("is_phone() expects 1 argument"));
5197 }
5198 let s = match &args[0] {
5199 VmValue::String(s) => s.to_string(),
5200 _ => return Err(runtime_err("is_phone() arg must be a string")),
5201 };
5202 Ok(VmValue::Bool(tl_data::validate::is_phone(&s)))
5203 }
5204 #[cfg(feature = "native")]
5205 BuiltinId::IsBetween => {
5206 if args.len() < 3 {
5207 return Err(runtime_err("is_between() expects (value, low, high)"));
5208 }
5209 let val = match &args[0] {
5210 VmValue::Int(n) => *n as f64,
5211 VmValue::Float(f) => *f,
5212 _ => return Err(runtime_err("is_between() value must be a number")),
5213 };
5214 let low = match &args[1] {
5215 VmValue::Int(n) => *n as f64,
5216 VmValue::Float(f) => *f,
5217 _ => return Err(runtime_err("is_between() low must be a number")),
5218 };
5219 let high = match &args[2] {
5220 VmValue::Int(n) => *n as f64,
5221 VmValue::Float(f) => *f,
5222 _ => return Err(runtime_err("is_between() high must be a number")),
5223 };
5224 Ok(VmValue::Bool(tl_data::validate::is_between(val, low, high)))
5225 }
5226 #[cfg(feature = "native")]
5227 BuiltinId::Levenshtein => {
5228 if args.len() < 2 {
5229 return Err(runtime_err("levenshtein() expects (str_a, str_b)"));
5230 }
5231 let a = match &args[0] {
5232 VmValue::String(s) => s.to_string(),
5233 _ => return Err(runtime_err("levenshtein() args must be strings")),
5234 };
5235 let b = match &args[1] {
5236 VmValue::String(s) => s.to_string(),
5237 _ => return Err(runtime_err("levenshtein() args must be strings")),
5238 };
5239 Ok(VmValue::Int(tl_data::validate::levenshtein(&a, &b) as i64))
5240 }
5241 #[cfg(feature = "native")]
5242 BuiltinId::Soundex => {
5243 if args.is_empty() {
5244 return Err(runtime_err("soundex() expects 1 argument"));
5245 }
5246 let s = match &args[0] {
5247 VmValue::String(s) => s.to_string(),
5248 _ => return Err(runtime_err("soundex() arg must be a string")),
5249 };
5250 Ok(VmValue::String(Arc::from(
5251 tl_data::validate::soundex(&s).as_str(),
5252 )))
5253 }
5254 #[cfg(not(feature = "native"))]
5255 BuiltinId::IsEmail
5256 | BuiltinId::IsUrl
5257 | BuiltinId::IsPhone
5258 | BuiltinId::IsBetween
5259 | BuiltinId::Levenshtein
5260 | BuiltinId::Soundex => Err(runtime_err("Data validation not available in WASM")),
5261 #[cfg(feature = "native")]
5262 BuiltinId::ReadMysql => {
5263 #[cfg(feature = "mysql")]
5264 {
5265 if args.len() < 2 {
5266 return Err(runtime_err("read_mysql() expects (conn_str, query)"));
5267 }
5268 let conn_str = match &args[0] {
5269 VmValue::String(s) => s.to_string(),
5270 _ => return Err(runtime_err("read_mysql() conn_str must be a string")),
5271 };
5272 let query = match &args[1] {
5273 VmValue::String(s) => s.to_string(),
5274 _ => return Err(runtime_err("read_mysql() query must be a string")),
5275 };
5276 let df = self
5277 .engine()
5278 .read_mysql(&conn_str, &query)
5279 .map_err(runtime_err)?;
5280 Ok(VmValue::Table(VmTable { df }))
5281 }
5282 #[cfg(not(feature = "mysql"))]
5283 Err(runtime_err("read_mysql() requires the 'mysql' feature"))
5284 }
5285 #[cfg(feature = "native")]
5286 BuiltinId::ReadSqlite => {
5287 #[cfg(feature = "sqlite")]
5288 {
5289 if args.len() < 2 {
5290 return Err(runtime_err("read_sqlite() expects (db_path, query)"));
5291 }
5292 let db_path = match &args[0] {
5293 VmValue::String(s) => s.to_string(),
5294 _ => return Err(runtime_err("read_sqlite() db_path must be a string")),
5295 };
5296 let query = match &args[1] {
5297 VmValue::String(s) => s.to_string(),
5298 _ => return Err(runtime_err("read_sqlite() query must be a string")),
5299 };
5300 let df = self
5301 .engine()
5302 .read_sqlite(&db_path, &query)
5303 .map_err(runtime_err)?;
5304 Ok(VmValue::Table(VmTable { df }))
5305 }
5306 #[cfg(not(feature = "sqlite"))]
5307 Err(runtime_err("read_sqlite() requires the 'sqlite' feature"))
5308 }
5309 #[cfg(feature = "native")]
5310 BuiltinId::WriteSqlite => {
5311 #[cfg(feature = "sqlite")]
5312 {
5313 if args.len() < 3 {
5314 return Err(runtime_err(
5315 "write_sqlite() expects (table, db_path, table_name)",
5316 ));
5317 }
5318 let df = match &args[0] {
5319 VmValue::Table(t) => t.df.clone(),
5320 _ => return Err(runtime_err("write_sqlite() first arg must be a table")),
5321 };
5322 let db_path = match &args[1] {
5323 VmValue::String(s) => s.to_string(),
5324 _ => return Err(runtime_err("write_sqlite() db_path must be a string")),
5325 };
5326 let table_name = match &args[2] {
5327 VmValue::String(s) => s.to_string(),
5328 _ => return Err(runtime_err("write_sqlite() table_name must be a string")),
5329 };
5330 self.engine()
5331 .write_sqlite(df, &db_path, &table_name)
5332 .map_err(runtime_err)?;
5333 Ok(VmValue::None)
5334 }
5335 #[cfg(not(feature = "sqlite"))]
5336 Err(runtime_err("write_sqlite() requires the 'sqlite' feature"))
5337 }
5338 #[cfg(feature = "native")]
5339 BuiltinId::ReadDuckDb => {
5340 #[cfg(feature = "duckdb")]
5341 {
5342 if args.len() < 2 {
5343 return Err(runtime_err("duckdb() expects (db_path, query)"));
5344 }
5345 let db_path = match &args[0] {
5346 VmValue::String(s) => s.to_string(),
5347 _ => return Err(runtime_err("duckdb() db_path must be a string")),
5348 };
5349 let query = match &args[1] {
5350 VmValue::String(s) => s.to_string(),
5351 _ => return Err(runtime_err("duckdb() query must be a string")),
5352 };
5353 let df = self
5354 .engine()
5355 .read_duckdb(&db_path, &query)
5356 .map_err(runtime_err)?;
5357 Ok(VmValue::Table(VmTable { df }))
5358 }
5359 #[cfg(not(feature = "duckdb"))]
5360 Err(runtime_err("duckdb() requires the 'duckdb' feature"))
5361 }
5362 #[cfg(feature = "native")]
5363 BuiltinId::WriteDuckDb => {
5364 #[cfg(feature = "duckdb")]
5365 {
5366 if args.len() < 3 {
5367 return Err(runtime_err(
5368 "write_duckdb() expects (table, db_path, table_name)",
5369 ));
5370 }
5371 let df = match &args[0] {
5372 VmValue::Table(t) => t.df.clone(),
5373 _ => return Err(runtime_err("write_duckdb() first arg must be a table")),
5374 };
5375 let db_path = match &args[1] {
5376 VmValue::String(s) => s.to_string(),
5377 _ => return Err(runtime_err("write_duckdb() db_path must be a string")),
5378 };
5379 let table_name = match &args[2] {
5380 VmValue::String(s) => s.to_string(),
5381 _ => return Err(runtime_err("write_duckdb() table_name must be a string")),
5382 };
5383 self.engine()
5384 .write_duckdb(df, &db_path, &table_name)
5385 .map_err(runtime_err)?;
5386 Ok(VmValue::None)
5387 }
5388 #[cfg(not(feature = "duckdb"))]
5389 Err(runtime_err("write_duckdb() requires the 'duckdb' feature"))
5390 }
5391 #[cfg(feature = "native")]
5392 BuiltinId::ReadIceberg => {
5393 #[cfg(feature = "iceberg")]
5394 {
5395 if args.is_empty() {
5396 return Err(runtime_err(
5397 "iceberg() expects (metadata_location, [columns | snapshot_id | props], [snapshot_id])",
5398 ));
5399 }
5400 let location = match &args[0] {
5401 VmValue::String(s) => s.to_string(),
5402 _ => {
5403 return Err(runtime_err(
5404 "iceberg() metadata_location must be a string",
5405 ));
5406 }
5407 };
5408 let mut opts = tl_data::IcebergReadOptions::default();
5414 match args.get(1) {
5415 None | Some(VmValue::None) => {}
5416 Some(VmValue::List(items)) => {
5417 opts.columns = items.iter().map(|x| format!("{x}")).collect();
5418 }
5419 Some(VmValue::Int(n)) => opts.snapshot_id = Some(*n),
5420 Some(VmValue::Map(entries)) => {
5421 for (k, v) in entries.iter() {
5422 match k.as_ref() {
5423 "columns" => {
5424 if let VmValue::List(items) = v {
5425 opts.columns =
5426 items.iter().map(|x| format!("{x}")).collect();
5427 } else {
5428 return Err(runtime_err(
5429 "iceberg() 'columns' must be a list",
5430 ));
5431 }
5432 }
5433 "snapshot_id" => {
5434 if let VmValue::Int(n) = v {
5435 opts.snapshot_id = Some(*n);
5436 } else {
5437 return Err(runtime_err(
5438 "iceberg() 'snapshot_id' must be an integer",
5439 ));
5440 }
5441 }
5442 _ => opts.props.push((k.to_string(), format!("{v}"))),
5443 }
5444 }
5445 }
5446 Some(_) => {
5447 return Err(runtime_err(
5448 "iceberg() second arg must be a column list, a snapshot_id int, or a props map",
5449 ));
5450 }
5451 }
5452 match args.get(2) {
5454 None | Some(VmValue::None) => {}
5455 Some(VmValue::Int(n)) => opts.snapshot_id = Some(*n),
5456 Some(_) => {
5457 return Err(runtime_err(
5458 "iceberg() third arg (snapshot_id) must be an integer",
5459 ));
5460 }
5461 }
5462 let df = self
5463 .engine()
5464 .read_iceberg(&location, opts)
5465 .map_err(runtime_err)?;
5466 Ok(VmValue::Table(VmTable { df }))
5467 }
5468 #[cfg(not(feature = "iceberg"))]
5469 Err(runtime_err("iceberg() requires the 'iceberg' feature"))
5470 }
5471 #[cfg(feature = "native")]
5472 BuiltinId::IcebergSnapshots | BuiltinId::IcebergSchema => {
5473 #[cfg(feature = "iceberg")]
5474 {
5475 let fname = if matches!(builtin_id, BuiltinId::IcebergSnapshots) {
5476 "iceberg_snapshots"
5477 } else {
5478 "iceberg_schema"
5479 };
5480 let location = match args.first() {
5481 Some(VmValue::String(s)) => s.to_string(),
5482 _ => {
5483 return Err(runtime_err(format!(
5484 "{fname}() expects (metadata_location, [props_map])"
5485 )));
5486 }
5487 };
5488 let props: Vec<(String, String)> = match args.get(1) {
5489 None | Some(VmValue::None) => Vec::new(),
5490 Some(VmValue::Map(entries)) => entries
5491 .iter()
5492 .map(|(k, v)| (k.to_string(), format!("{v}")))
5493 .collect(),
5494 Some(_) => {
5495 return Err(runtime_err(format!("{fname}() props must be a map")));
5496 }
5497 };
5498 let df = if matches!(builtin_id, BuiltinId::IcebergSnapshots) {
5499 self.engine().iceberg_snapshots(&location, props)
5500 } else {
5501 self.engine().iceberg_schema(&location, props)
5502 }
5503 .map_err(runtime_err)?;
5504 Ok(VmValue::Table(VmTable { df }))
5505 }
5506 #[cfg(not(feature = "iceberg"))]
5507 Err(runtime_err(
5508 "iceberg_snapshots()/iceberg_schema() require the 'iceberg' feature",
5509 ))
5510 }
5511 #[cfg(feature = "native")]
5512 BuiltinId::ReadRedshift => {
5513 if args.len() < 2 {
5514 return Err(runtime_err("redshift() expects (conn_str, query)"));
5515 }
5516 let conn_str = match &args[0] {
5517 VmValue::String(s) => {
5518 let s_str = s.to_string();
5519 resolve_tl_config_connection(&s_str)
5520 }
5521 _ => return Err(runtime_err("redshift() conn_str must be a string")),
5522 };
5523 let query = match &args[1] {
5524 VmValue::String(s) => s.to_string(),
5525 _ => return Err(runtime_err("redshift() query must be a string")),
5526 };
5527 let df = self
5528 .engine()
5529 .read_redshift(&conn_str, &query)
5530 .map_err(runtime_err)?;
5531 Ok(VmValue::Table(VmTable { df }))
5532 }
5533 #[cfg(feature = "native")]
5534 BuiltinId::ReadMssql => {
5535 #[cfg(feature = "mssql")]
5536 {
5537 if args.len() < 2 {
5538 return Err(runtime_err("mssql() expects (conn_str, query)"));
5539 }
5540 let conn_str = match &args[0] {
5541 VmValue::String(s) => {
5542 let s_str = s.to_string();
5543 resolve_tl_config_connection(&s_str)
5544 }
5545 _ => return Err(runtime_err("mssql() conn_str must be a string")),
5546 };
5547 let query = match &args[1] {
5548 VmValue::String(s) => s.to_string(),
5549 _ => return Err(runtime_err("mssql() query must be a string")),
5550 };
5551 let df = self
5552 .engine()
5553 .read_mssql(&conn_str, &query)
5554 .map_err(runtime_err)?;
5555 Ok(VmValue::Table(VmTable { df }))
5556 }
5557 #[cfg(not(feature = "mssql"))]
5558 Err(runtime_err("mssql() requires the 'mssql' feature"))
5559 }
5560 #[cfg(feature = "native")]
5561 BuiltinId::ReadSnowflake => {
5562 #[cfg(feature = "snowflake")]
5563 {
5564 if args.len() < 2 {
5565 return Err(runtime_err("snowflake() expects (config, query)"));
5566 }
5567 let config = match &args[0] {
5568 VmValue::String(s) => {
5569 let s_str = s.to_string();
5570 resolve_tl_config_connection(&s_str)
5571 }
5572 _ => return Err(runtime_err("snowflake() config must be a string")),
5573 };
5574 let query = match &args[1] {
5575 VmValue::String(s) => s.to_string(),
5576 _ => return Err(runtime_err("snowflake() query must be a string")),
5577 };
5578 let df = self
5579 .engine()
5580 .read_snowflake(&config, &query)
5581 .map_err(runtime_err)?;
5582 Ok(VmValue::Table(VmTable { df }))
5583 }
5584 #[cfg(not(feature = "snowflake"))]
5585 Err(runtime_err("snowflake() requires the 'snowflake' feature"))
5586 }
5587 #[cfg(feature = "native")]
5588 BuiltinId::ReadBigQuery => {
5589 #[cfg(feature = "bigquery")]
5590 {
5591 if args.len() < 2 {
5592 return Err(runtime_err("bigquery() expects (config, query)"));
5593 }
5594 let config = match &args[0] {
5595 VmValue::String(s) => {
5596 let s_str = s.to_string();
5597 resolve_tl_config_connection(&s_str)
5598 }
5599 _ => return Err(runtime_err("bigquery() config must be a string")),
5600 };
5601 let query = match &args[1] {
5602 VmValue::String(s) => s.to_string(),
5603 _ => return Err(runtime_err("bigquery() query must be a string")),
5604 };
5605 let df = self
5606 .engine()
5607 .read_bigquery(&config, &query)
5608 .map_err(runtime_err)?;
5609 Ok(VmValue::Table(VmTable { df }))
5610 }
5611 #[cfg(not(feature = "bigquery"))]
5612 Err(runtime_err("bigquery() requires the 'bigquery' feature"))
5613 }
5614 #[cfg(feature = "native")]
5615 BuiltinId::ReadDatabricks => {
5616 #[cfg(feature = "databricks")]
5617 {
5618 if args.len() < 2 {
5619 return Err(runtime_err("databricks() expects (config, query)"));
5620 }
5621 let config = match &args[0] {
5622 VmValue::String(s) => {
5623 let s_str = s.to_string();
5624 resolve_tl_config_connection(&s_str)
5625 }
5626 _ => return Err(runtime_err("databricks() config must be a string")),
5627 };
5628 let query = match &args[1] {
5629 VmValue::String(s) => s.to_string(),
5630 _ => return Err(runtime_err("databricks() query must be a string")),
5631 };
5632 let df = self
5633 .engine()
5634 .read_databricks(&config, &query)
5635 .map_err(runtime_err)?;
5636 Ok(VmValue::Table(VmTable { df }))
5637 }
5638 #[cfg(not(feature = "databricks"))]
5639 Err(runtime_err(
5640 "databricks() requires the 'databricks' feature",
5641 ))
5642 }
5643 #[cfg(feature = "native")]
5644 BuiltinId::ReadClickHouse => {
5645 #[cfg(feature = "clickhouse")]
5646 {
5647 if args.len() < 2 {
5648 return Err(runtime_err("clickhouse() expects (url, query)"));
5649 }
5650 let url = match &args[0] {
5651 VmValue::String(s) => {
5652 let s_str = s.to_string();
5653 resolve_tl_config_connection(&s_str)
5654 }
5655 _ => return Err(runtime_err("clickhouse() url must be a string")),
5656 };
5657 let query = match &args[1] {
5658 VmValue::String(s) => s.to_string(),
5659 _ => return Err(runtime_err("clickhouse() query must be a string")),
5660 };
5661 let df = self
5662 .engine()
5663 .read_clickhouse(&url, &query)
5664 .map_err(runtime_err)?;
5665 Ok(VmValue::Table(VmTable { df }))
5666 }
5667 #[cfg(not(feature = "clickhouse"))]
5668 Err(runtime_err(
5669 "clickhouse() requires the 'clickhouse' feature",
5670 ))
5671 }
5672 #[cfg(feature = "native")]
5673 BuiltinId::ReadMongo => {
5674 #[cfg(feature = "mongodb")]
5675 {
5676 if args.len() < 4 {
5677 return Err(runtime_err(
5678 "mongo() expects (conn_str, database, collection, filter_json)",
5679 ));
5680 }
5681 let conn_str = match &args[0] {
5682 VmValue::String(s) => {
5683 let s_str = s.to_string();
5684 resolve_tl_config_connection(&s_str)
5685 }
5686 _ => return Err(runtime_err("mongo() conn_str must be a string")),
5687 };
5688 let database = match &args[1] {
5689 VmValue::String(s) => s.to_string(),
5690 _ => return Err(runtime_err("mongo() database must be a string")),
5691 };
5692 let collection = match &args[2] {
5693 VmValue::String(s) => s.to_string(),
5694 _ => return Err(runtime_err("mongo() collection must be a string")),
5695 };
5696 let filter_json = match &args[3] {
5697 VmValue::String(s) => s.to_string(),
5698 _ => return Err(runtime_err("mongo() filter must be a string")),
5699 };
5700 let df = self
5701 .engine()
5702 .read_mongo(&conn_str, &database, &collection, &filter_json)
5703 .map_err(runtime_err)?;
5704 Ok(VmValue::Table(VmTable { df }))
5705 }
5706 #[cfg(not(feature = "mongodb"))]
5707 Err(runtime_err("mongo() requires the 'mongodb' feature"))
5708 }
5709 #[cfg(feature = "native")]
5710 BuiltinId::SftpDownload => {
5711 #[cfg(feature = "sftp")]
5712 {
5713 if args.len() < 3 {
5714 return Err(runtime_err(
5715 "sftp_download() expects (config, remote_path, local_path)",
5716 ));
5717 }
5718 let config = match &args[0] {
5719 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5720 _ => return Err(runtime_err("sftp_download() config must be a string")),
5721 };
5722 let remote = match &args[1] {
5723 VmValue::String(s) => s.to_string(),
5724 _ => {
5725 return Err(runtime_err(
5726 "sftp_download() remote_path must be a string",
5727 ));
5728 }
5729 };
5730 let local = match &args[2] {
5731 VmValue::String(s) => s.to_string(),
5732 _ => {
5733 return Err(runtime_err("sftp_download() local_path must be a string"));
5734 }
5735 };
5736 let result = self
5737 .engine()
5738 .sftp_download(&config, &remote, &local)
5739 .map_err(runtime_err)?;
5740 Ok(VmValue::String(Arc::from(result.as_str())))
5741 }
5742 #[cfg(not(feature = "sftp"))]
5743 Err(runtime_err("sftp_download() requires the 'sftp' feature"))
5744 }
5745 #[cfg(feature = "native")]
5746 BuiltinId::SftpUpload => {
5747 #[cfg(feature = "sftp")]
5748 {
5749 if args.len() < 3 {
5750 return Err(runtime_err(
5751 "sftp_upload() expects (config, local_path, remote_path)",
5752 ));
5753 }
5754 let config = match &args[0] {
5755 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5756 _ => return Err(runtime_err("sftp_upload() config must be a string")),
5757 };
5758 let local = match &args[1] {
5759 VmValue::String(s) => s.to_string(),
5760 _ => return Err(runtime_err("sftp_upload() local_path must be a string")),
5761 };
5762 let remote = match &args[2] {
5763 VmValue::String(s) => s.to_string(),
5764 _ => return Err(runtime_err("sftp_upload() remote_path must be a string")),
5765 };
5766 let result = self
5767 .engine()
5768 .sftp_upload(&config, &local, &remote)
5769 .map_err(runtime_err)?;
5770 Ok(VmValue::String(Arc::from(result.as_str())))
5771 }
5772 #[cfg(not(feature = "sftp"))]
5773 Err(runtime_err("sftp_upload() requires the 'sftp' feature"))
5774 }
5775 #[cfg(feature = "native")]
5776 BuiltinId::SftpList => {
5777 #[cfg(feature = "sftp")]
5778 {
5779 if args.len() < 2 {
5780 return Err(runtime_err("sftp_list() expects (config, remote_path)"));
5781 }
5782 let config = match &args[0] {
5783 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5784 _ => return Err(runtime_err("sftp_list() config must be a string")),
5785 };
5786 let remote = match &args[1] {
5787 VmValue::String(s) => s.to_string(),
5788 _ => return Err(runtime_err("sftp_list() remote_path must be a string")),
5789 };
5790 let df = self
5791 .engine()
5792 .sftp_list(&config, &remote)
5793 .map_err(runtime_err)?;
5794 Ok(VmValue::Table(VmTable { df }))
5795 }
5796 #[cfg(not(feature = "sftp"))]
5797 Err(runtime_err("sftp_list() requires the 'sftp' feature"))
5798 }
5799 #[cfg(feature = "native")]
5800 BuiltinId::SftpReadCsv => {
5801 #[cfg(feature = "sftp")]
5802 {
5803 if args.len() < 2 {
5804 return Err(runtime_err("sftp_read_csv() expects (config, remote_path)"));
5805 }
5806 let config = match &args[0] {
5807 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5808 _ => return Err(runtime_err("sftp_read_csv() config must be a string")),
5809 };
5810 let remote = match &args[1] {
5811 VmValue::String(s) => s.to_string(),
5812 _ => {
5813 return Err(runtime_err(
5814 "sftp_read_csv() remote_path must be a string",
5815 ));
5816 }
5817 };
5818 let df = self
5819 .engine()
5820 .sftp_read_csv(&config, &remote)
5821 .map_err(runtime_err)?;
5822 Ok(VmValue::Table(VmTable { df }))
5823 }
5824 #[cfg(not(feature = "sftp"))]
5825 Err(runtime_err("sftp_read_csv() requires the 'sftp' feature"))
5826 }
5827 #[cfg(feature = "native")]
5828 BuiltinId::SftpReadParquet => {
5829 #[cfg(feature = "sftp")]
5830 {
5831 if args.len() < 2 {
5832 return Err(runtime_err(
5833 "sftp_read_parquet() expects (config, remote_path)",
5834 ));
5835 }
5836 let config = match &args[0] {
5837 VmValue::String(s) => resolve_tl_config_connection(&s.to_string()),
5838 _ => {
5839 return Err(runtime_err("sftp_read_parquet() config must be a string"));
5840 }
5841 };
5842 let remote = match &args[1] {
5843 VmValue::String(s) => s.to_string(),
5844 _ => {
5845 return Err(runtime_err(
5846 "sftp_read_parquet() remote_path must be a string",
5847 ));
5848 }
5849 };
5850 let df = self
5851 .engine()
5852 .sftp_read_parquet(&config, &remote)
5853 .map_err(runtime_err)?;
5854 Ok(VmValue::Table(VmTable { df }))
5855 }
5856 #[cfg(not(feature = "sftp"))]
5857 Err(runtime_err(
5858 "sftp_read_parquet() requires the 'sftp' feature",
5859 ))
5860 }
5861 #[cfg(feature = "native")]
5862 BuiltinId::RedisConnect => {
5863 #[cfg(feature = "redis")]
5864 {
5865 if args.is_empty() {
5866 return Err(runtime_err("redis_connect() expects (url)"));
5867 }
5868 let url = match &args[0] {
5869 VmValue::String(s) => s.to_string(),
5870 _ => return Err(runtime_err("redis_connect() url must be a string")),
5871 };
5872 let result = tl_data::redis_conn::redis_connect(&url).map_err(runtime_err)?;
5873 Ok(VmValue::String(Arc::from(result.as_str())))
5874 }
5875 #[cfg(not(feature = "redis"))]
5876 Err(runtime_err("redis_connect() requires the 'redis' feature"))
5877 }
5878 #[cfg(feature = "native")]
5879 BuiltinId::RedisGet => {
5880 #[cfg(feature = "redis")]
5881 {
5882 if args.len() < 2 {
5883 return Err(runtime_err("redis_get() expects (url, key)"));
5884 }
5885 let url = match &args[0] {
5886 VmValue::String(s) => s.to_string(),
5887 _ => return Err(runtime_err("redis_get() url must be a string")),
5888 };
5889 let key = match &args[1] {
5890 VmValue::String(s) => s.to_string(),
5891 _ => return Err(runtime_err("redis_get() key must be a string")),
5892 };
5893 match tl_data::redis_conn::redis_get(&url, &key).map_err(runtime_err)? {
5894 Some(v) => Ok(VmValue::String(Arc::from(v.as_str()))),
5895 None => Ok(VmValue::None),
5896 }
5897 }
5898 #[cfg(not(feature = "redis"))]
5899 Err(runtime_err("redis_get() requires the 'redis' feature"))
5900 }
5901 #[cfg(feature = "native")]
5902 BuiltinId::RedisSet => {
5903 #[cfg(feature = "redis")]
5904 {
5905 if args.len() < 3 {
5906 return Err(runtime_err("redis_set() expects (url, key, value)"));
5907 }
5908 let url = match &args[0] {
5909 VmValue::String(s) => s.to_string(),
5910 _ => return Err(runtime_err("redis_set() url must be a string")),
5911 };
5912 let key = match &args[1] {
5913 VmValue::String(s) => s.to_string(),
5914 _ => return Err(runtime_err("redis_set() key must be a string")),
5915 };
5916 let value = match &args[2] {
5917 VmValue::String(s) => s.to_string(),
5918 _ => format!("{}", &args[2]),
5919 };
5920 tl_data::redis_conn::redis_set(&url, &key, &value).map_err(runtime_err)?;
5921 Ok(VmValue::None)
5922 }
5923 #[cfg(not(feature = "redis"))]
5924 Err(runtime_err("redis_set() requires the 'redis' feature"))
5925 }
5926 #[cfg(feature = "native")]
5927 BuiltinId::RedisDel => {
5928 #[cfg(feature = "redis")]
5929 {
5930 if args.len() < 2 {
5931 return Err(runtime_err("redis_del() expects (url, key)"));
5932 }
5933 let url = match &args[0] {
5934 VmValue::String(s) => s.to_string(),
5935 _ => return Err(runtime_err("redis_del() url must be a string")),
5936 };
5937 let key = match &args[1] {
5938 VmValue::String(s) => s.to_string(),
5939 _ => return Err(runtime_err("redis_del() key must be a string")),
5940 };
5941 let deleted =
5942 tl_data::redis_conn::redis_del(&url, &key).map_err(runtime_err)?;
5943 Ok(VmValue::Bool(deleted))
5944 }
5945 #[cfg(not(feature = "redis"))]
5946 Err(runtime_err("redis_del() requires the 'redis' feature"))
5947 }
5948 #[cfg(feature = "native")]
5949 BuiltinId::GraphqlQuery => {
5950 if args.len() < 2 {
5951 return Err(runtime_err(
5952 "graphql_query() expects (endpoint, query, [variables])",
5953 ));
5954 }
5955 let endpoint = match &args[0] {
5956 VmValue::String(s) => s.to_string(),
5957 _ => return Err(runtime_err("graphql_query() endpoint must be a string")),
5958 };
5959 let query = match &args[1] {
5960 VmValue::String(s) => s.to_string(),
5961 _ => return Err(runtime_err("graphql_query() query must be a string")),
5962 };
5963 let variables = if args.len() > 2 {
5964 vm_value_to_json(&args[2])
5965 } else {
5966 serde_json::Value::Null
5967 };
5968 let mut body = serde_json::Map::new();
5969 body.insert("query".to_string(), serde_json::Value::String(query));
5970 if !variables.is_null() {
5971 body.insert("variables".to_string(), variables);
5972 }
5973 let client = reqwest::blocking::Client::new();
5974 let resp = client
5975 .post(&endpoint)
5976 .header("Content-Type", "application/json")
5977 .json(&body)
5978 .send()
5979 .map_err(|e| runtime_err(format!("graphql_query() request error: {e}")))?;
5980 let text = resp
5981 .text()
5982 .map_err(|e| runtime_err(format!("graphql_query() response error: {e}")))?;
5983 let json: serde_json::Value = serde_json::from_str(&text)
5984 .map_err(|e| runtime_err(format!("graphql_query() JSON parse error: {e}")))?;
5985 Ok(vm_json_to_value(&json))
5986 }
5987 #[cfg(feature = "native")]
5988 BuiltinId::RegisterS3 => {
5989 #[cfg(feature = "s3")]
5990 {
5991 if args.len() < 2 {
5992 return Err(runtime_err(
5993 "register_s3() expects (bucket, region, [access_key], [secret_key], [endpoint])",
5994 ));
5995 }
5996 let bucket = match &args[0] {
5997 VmValue::String(s) => s.to_string(),
5998 _ => return Err(runtime_err("register_s3() bucket must be a string")),
5999 };
6000 let region = match &args[1] {
6001 VmValue::String(s) => s.to_string(),
6002 _ => return Err(runtime_err("register_s3() region must be a string")),
6003 };
6004 let access_key = args.get(2).and_then(|v| {
6005 if let VmValue::String(s) = v {
6006 Some(s.to_string())
6007 } else {
6008 None
6009 }
6010 });
6011 let secret_key = args.get(3).and_then(|v| {
6012 if let VmValue::String(s) = v {
6013 Some(s.to_string())
6014 } else {
6015 None
6016 }
6017 });
6018 let endpoint = args.get(4).and_then(|v| {
6019 if let VmValue::String(s) = v {
6020 Some(s.to_string())
6021 } else {
6022 None
6023 }
6024 });
6025 self.engine()
6026 .register_s3(
6027 &bucket,
6028 ®ion,
6029 access_key.as_deref(),
6030 secret_key.as_deref(),
6031 endpoint.as_deref(),
6032 )
6033 .map_err(runtime_err)?;
6034 Ok(VmValue::None)
6035 }
6036 #[cfg(not(feature = "s3"))]
6037 Err(runtime_err("register_s3() requires the 's3' feature"))
6038 }
6039 #[cfg(not(feature = "native"))]
6040 BuiltinId::ReadMysql
6041 | BuiltinId::ReadSqlite
6042 | BuiltinId::WriteSqlite
6043 | BuiltinId::ReadDuckDb
6044 | BuiltinId::WriteDuckDb
6045 | BuiltinId::ReadIceberg
6046 | BuiltinId::IcebergSnapshots
6047 | BuiltinId::IcebergSchema
6048 | BuiltinId::ReadRedshift
6049 | BuiltinId::ReadMssql
6050 | BuiltinId::ReadSnowflake
6051 | BuiltinId::ReadBigQuery
6052 | BuiltinId::ReadDatabricks
6053 | BuiltinId::ReadClickHouse
6054 | BuiltinId::ReadMongo
6055 | BuiltinId::SftpDownload
6056 | BuiltinId::SftpUpload
6057 | BuiltinId::SftpList
6058 | BuiltinId::SftpReadCsv
6059 | BuiltinId::SftpReadParquet
6060 | BuiltinId::RedisConnect
6061 | BuiltinId::RedisGet
6062 | BuiltinId::RedisSet
6063 | BuiltinId::RedisDel
6064 | BuiltinId::GraphqlQuery
6065 | BuiltinId::RegisterS3 => Err(runtime_err("Connectors not available in WASM")),
6066 BuiltinId::PyImport => {
6068 self.check_permission("python")?;
6069 #[cfg(feature = "python")]
6070 {
6071 crate::python::py_import_impl(&args)
6072 }
6073 #[cfg(not(feature = "python"))]
6074 Err(runtime_err("py_import() requires the 'python' feature"))
6075 }
6076 BuiltinId::PyCall => {
6077 self.check_permission("python")?;
6078 #[cfg(feature = "python")]
6079 {
6080 crate::python::py_call_impl(&args)
6081 }
6082 #[cfg(not(feature = "python"))]
6083 Err(runtime_err("py_call() requires the 'python' feature"))
6084 }
6085 BuiltinId::PyEval => {
6086 self.check_permission("python")?;
6087 #[cfg(feature = "python")]
6088 {
6089 crate::python::py_eval_impl(&args)
6090 }
6091 #[cfg(not(feature = "python"))]
6092 Err(runtime_err("py_eval() requires the 'python' feature"))
6093 }
6094 BuiltinId::PyGetAttr => {
6095 self.check_permission("python")?;
6096 #[cfg(feature = "python")]
6097 {
6098 crate::python::py_getattr_impl(&args)
6099 }
6100 #[cfg(not(feature = "python"))]
6101 Err(runtime_err("py_getattr() requires the 'python' feature"))
6102 }
6103 BuiltinId::PySetAttr => {
6104 self.check_permission("python")?;
6105 #[cfg(feature = "python")]
6106 {
6107 crate::python::py_setattr_impl(&args)
6108 }
6109 #[cfg(not(feature = "python"))]
6110 Err(runtime_err("py_setattr() requires the 'python' feature"))
6111 }
6112 BuiltinId::PyToTl => {
6113 #[cfg(feature = "python")]
6114 {
6115 crate::python::py_to_tl_impl(&args)
6116 }
6117 #[cfg(not(feature = "python"))]
6118 Err(runtime_err("py_to_tl() requires the 'python' feature"))
6119 }
6120
6121 #[cfg(feature = "native")]
6123 BuiltinId::SchemaRegister => {
6124 let name = match args.first() {
6125 Some(VmValue::String(s)) => s.to_string(),
6126 _ => {
6127 return Err(runtime_err(
6128 "schema_register: first arg must be schema name string",
6129 ));
6130 }
6131 };
6132 let version = match args.get(1) {
6133 Some(VmValue::Int(v)) => *v,
6134 _ => {
6135 return Err(runtime_err(
6136 "schema_register: second arg must be version number",
6137 ));
6138 }
6139 };
6140 let fields = match args.get(2) {
6141 Some(VmValue::Map(pairs)) => {
6142 let mut arrow_fields = Vec::new();
6143 for (k, v) in pairs.iter() {
6144 let fname = k.to_string();
6145 let ftype = match v {
6146 VmValue::String(s) => s.to_string(),
6147 _ => "string".to_string(),
6148 };
6149 arrow_fields.push(tl_data::ArrowField::new(
6150 &fname,
6151 crate::schema::type_name_to_arrow_pub(&ftype),
6152 true,
6153 ));
6154 }
6155 arrow_fields
6156 }
6157 _ => return Err(runtime_err("schema_register: third arg must be fields map")),
6158 };
6159 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(fields));
6160 self.schema_registry
6161 .register(
6162 &name,
6163 version,
6164 schema,
6165 crate::schema::SchemaMetadata::default(),
6166 )
6167 .map_err(|e| runtime_err(&e))?;
6168 Ok(VmValue::None)
6169 }
6170 #[cfg(feature = "native")]
6171 BuiltinId::SchemaGet => {
6172 let name = match args.first() {
6173 Some(VmValue::String(s)) => s.to_string(),
6174 _ => return Err(runtime_err("schema_get: need name")),
6175 };
6176 let version = match args.get(1) {
6177 Some(VmValue::Int(v)) => *v,
6178 _ => return Err(runtime_err("schema_get: need version")),
6179 };
6180 match self.schema_registry.get(&name, version) {
6181 Some(vs) => {
6182 let fields: Vec<VmValue> = vs
6183 .schema
6184 .fields()
6185 .iter()
6186 .map(|f| {
6187 VmValue::String(std::sync::Arc::from(format!(
6188 "{}: {}",
6189 f.name(),
6190 f.data_type()
6191 )))
6192 })
6193 .collect();
6194 Ok(VmValue::List(Box::new(fields)))
6195 }
6196 None => Ok(VmValue::None),
6197 }
6198 }
6199 #[cfg(feature = "native")]
6200 BuiltinId::SchemaLatest => {
6201 let name = match args.first() {
6202 Some(VmValue::String(s)) => s.to_string(),
6203 _ => return Err(runtime_err("schema_latest: need name")),
6204 };
6205 match self.schema_registry.latest(&name) {
6206 Some(vs) => Ok(VmValue::Int(vs.version)),
6207 None => Ok(VmValue::None),
6208 }
6209 }
6210 #[cfg(feature = "native")]
6211 BuiltinId::SchemaHistory => {
6212 let name = match args.first() {
6213 Some(VmValue::String(s)) => s.to_string(),
6214 _ => return Err(runtime_err("schema_history: need name")),
6215 };
6216 let versions = self.schema_registry.versions(&name);
6217 Ok(VmValue::List(Box::new(
6218 versions.into_iter().map(VmValue::Int).collect(),
6219 )))
6220 }
6221 #[cfg(feature = "native")]
6222 BuiltinId::SchemaCheck => {
6223 let name = match args.first() {
6224 Some(VmValue::String(s)) => s.to_string(),
6225 _ => return Err(runtime_err("schema_check: need name")),
6226 };
6227 let v1 = match args.get(1) {
6228 Some(VmValue::Int(v)) => *v,
6229 _ => return Err(runtime_err("schema_check: need v1")),
6230 };
6231 let v2 = match args.get(2) {
6232 Some(VmValue::Int(v)) => *v,
6233 _ => return Err(runtime_err("schema_check: need v2")),
6234 };
6235 let mode_str = match args.get(3) {
6236 Some(VmValue::String(s)) => s.to_string(),
6237 _ => "backward".to_string(),
6238 };
6239 let mode = crate::schema::CompatibilityMode::from_str(&mode_str);
6240 let issues = self
6241 .schema_registry
6242 .check_compatibility(&name, v1, v2, mode);
6243 Ok(VmValue::List(Box::new(
6244 issues
6245 .into_iter()
6246 .map(|i| VmValue::String(std::sync::Arc::from(i.to_string())))
6247 .collect(),
6248 )))
6249 }
6250 #[cfg(feature = "native")]
6251 BuiltinId::SchemaDiff => {
6252 let name = match args.first() {
6253 Some(VmValue::String(s)) => s.to_string(),
6254 _ => return Err(runtime_err("schema_diff: need name")),
6255 };
6256 let v1 = match args.get(1) {
6257 Some(VmValue::Int(v)) => *v,
6258 _ => return Err(runtime_err("schema_diff: need v1")),
6259 };
6260 let v2 = match args.get(2) {
6261 Some(VmValue::Int(v)) => *v,
6262 _ => return Err(runtime_err("schema_diff: need v2")),
6263 };
6264 let diffs = self.schema_registry.diff(&name, v1, v2);
6265 Ok(VmValue::List(Box::new(
6266 diffs
6267 .into_iter()
6268 .map(|d| VmValue::String(std::sync::Arc::from(d.to_string())))
6269 .collect(),
6270 )))
6271 }
6272 #[cfg(feature = "native")]
6273 BuiltinId::SchemaApplyMigration => {
6274 let name = match args.first() {
6275 Some(VmValue::String(s)) => s.to_string(),
6276 _ => return Err(runtime_err("schema_apply_migration: need name")),
6277 };
6278 let from_v = match args.get(1) {
6279 Some(VmValue::Int(v)) => *v,
6280 _ => return Err(runtime_err("schema_apply_migration: need from_ver")),
6281 };
6282 let to_v = match args.get(2) {
6283 Some(VmValue::Int(v)) => *v,
6284 _ => return Err(runtime_err("schema_apply_migration: need to_ver")),
6285 };
6286 Ok(VmValue::String(std::sync::Arc::from(format!(
6287 "migration {}:{}->{} applied",
6288 name, from_v, to_v
6289 ))))
6290 }
6291 #[cfg(feature = "native")]
6292 BuiltinId::SchemaVersions => {
6293 let name = match args.first() {
6294 Some(VmValue::String(s)) => s.to_string(),
6295 _ => return Err(runtime_err("schema_versions: need name")),
6296 };
6297 let versions = self.schema_registry.versions(&name);
6298 Ok(VmValue::List(Box::new(
6299 versions.into_iter().map(VmValue::Int).collect(),
6300 )))
6301 }
6302 #[cfg(feature = "native")]
6303 BuiltinId::SchemaFields => {
6304 let name = match args.first() {
6305 Some(VmValue::String(s)) => s.to_string(),
6306 _ => return Err(runtime_err("schema_fields: need name")),
6307 };
6308 let version = match args.get(1) {
6309 Some(VmValue::Int(v)) => *v,
6310 _ => return Err(runtime_err("schema_fields: need version")),
6311 };
6312 let fields = self.schema_registry.fields(&name, version);
6313 Ok(VmValue::List(Box::new(
6314 fields
6315 .into_iter()
6316 .map(|(n, t)| {
6317 VmValue::String(std::sync::Arc::from(format!("{}: {}", n, t)))
6318 })
6319 .collect(),
6320 )))
6321 }
6322 #[cfg(not(feature = "native"))]
6323 BuiltinId::SchemaRegister
6324 | BuiltinId::SchemaGet
6325 | BuiltinId::SchemaLatest
6326 | BuiltinId::SchemaHistory
6327 | BuiltinId::SchemaCheck
6328 | BuiltinId::SchemaDiff
6329 | BuiltinId::SchemaApplyMigration
6330 | BuiltinId::SchemaVersions
6331 | BuiltinId::SchemaFields => {
6332 let _ = args;
6333 Err(runtime_err("Schema operations not available in WASM"))
6334 }
6335
6336 BuiltinId::Decimal => {
6338 use std::str::FromStr;
6339 let s = match args.first() {
6340 Some(VmValue::String(s)) => s.to_string(),
6341 Some(VmValue::Int(n)) => n.to_string(),
6342 Some(VmValue::Float(f)) => f.to_string(),
6343 _ => return Err(runtime_err("decimal(): expected string, int, or float")),
6344 };
6345 let d = rust_decimal::Decimal::from_str(&s)
6346 .map_err(|e| runtime_err(format!("decimal(): invalid: {e}")))?;
6347 Ok(VmValue::Decimal(d))
6348 }
6349
6350 BuiltinId::SecretGet => {
6352 let key = match args.first() {
6353 Some(VmValue::String(s)) => s.to_string(),
6354 _ => return Err(runtime_err("secret_get: need key")),
6355 };
6356 if let Some(val) = self.secret_vault.get(&key) {
6357 Ok(VmValue::Secret(Arc::from(val.as_str())))
6358 } else {
6359 let env_key = format!("TL_SECRET_{}", key.to_uppercase());
6361 match std::env::var(&env_key) {
6362 Ok(val) => Ok(VmValue::Secret(Arc::from(val.as_str()))),
6363 Err(_) => Ok(VmValue::None),
6364 }
6365 }
6366 }
6367 BuiltinId::SecretSet => {
6368 let key = match args.first() {
6369 Some(VmValue::String(s)) => s.to_string(),
6370 _ => return Err(runtime_err("secret_set: need key")),
6371 };
6372 let val = match args.get(1) {
6373 Some(VmValue::String(s)) => s.to_string(),
6374 Some(VmValue::Secret(s)) => s.to_string(),
6375 _ => return Err(runtime_err("secret_set: need value")),
6376 };
6377 self.secret_vault.insert(key, val);
6378 Ok(VmValue::None)
6379 }
6380 BuiltinId::SecretDelete => {
6381 let key = match args.first() {
6382 Some(VmValue::String(s)) => s.to_string(),
6383 _ => return Err(runtime_err("secret_delete: need key")),
6384 };
6385 self.secret_vault.remove(&key);
6386 Ok(VmValue::None)
6387 }
6388 BuiltinId::SecretList => {
6389 let keys: Vec<VmValue> = self
6390 .secret_vault
6391 .keys()
6392 .map(|k| VmValue::String(Arc::from(k.as_str())))
6393 .collect();
6394 Ok(VmValue::List(Box::new(keys)))
6395 }
6396 BuiltinId::CheckPermission => {
6397 let perm = match args.first() {
6398 Some(VmValue::String(s)) => s.to_string(),
6399 _ => return Err(runtime_err("check_permission: need permission name")),
6400 };
6401 let allowed = match self.security_policy {
6402 Some(ref policy) => policy.check(&perm),
6403 None => true,
6404 };
6405 Ok(VmValue::Bool(allowed))
6406 }
6407 BuiltinId::MaskEmail => {
6408 let email = match args.first() {
6409 Some(VmValue::String(s)) => s.to_string(),
6410 _ => return Err(runtime_err("mask_email: need string")),
6411 };
6412 let masked = if let Some(at_pos) = email.find('@') {
6413 let local = &email[..at_pos];
6414 let domain = &email[at_pos..];
6415 if local.len() > 1 {
6416 format!("{}***{}", &local[..1], domain)
6417 } else {
6418 format!("***{domain}")
6419 }
6420 } else {
6421 "***".to_string()
6422 };
6423 Ok(VmValue::String(Arc::from(masked.as_str())))
6424 }
6425 BuiltinId::MaskPhone => {
6426 let phone = match args.first() {
6427 Some(VmValue::String(s)) => s.to_string(),
6428 _ => return Err(runtime_err("mask_phone: need string")),
6429 };
6430 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
6431 let masked = if digits.len() >= 4 {
6432 let last4 = &digits[digits.len() - 4..];
6433 format!("***-***-{last4}")
6434 } else {
6435 "***".to_string()
6436 };
6437 Ok(VmValue::String(Arc::from(masked.as_str())))
6438 }
6439 BuiltinId::MaskCreditCard => {
6440 let cc = match args.first() {
6441 Some(VmValue::String(s)) => s.to_string(),
6442 _ => return Err(runtime_err("mask_cc: need string")),
6443 };
6444 let digits: String = cc.chars().filter(|c| c.is_ascii_digit()).collect();
6445 let masked = if digits.len() >= 4 {
6446 let last4 = &digits[digits.len() - 4..];
6447 format!("****-****-****-{last4}")
6448 } else {
6449 "****-****-****-****".to_string()
6450 };
6451 Ok(VmValue::String(Arc::from(masked.as_str())))
6452 }
6453 BuiltinId::Redact => {
6454 let val = match args.first() {
6455 Some(v) => format!("{v}"),
6456 _ => return Err(runtime_err("redact: need value")),
6457 };
6458 let policy = match args.get(1) {
6459 Some(VmValue::String(s)) => s.to_string(),
6460 _ => "full".to_string(),
6461 };
6462 let result = match policy.as_str() {
6463 "full" => "***".to_string(),
6464 "partial" => {
6465 if val.len() > 2 {
6466 format!("{}***{}", &val[..1], &val[val.len() - 1..])
6467 } else {
6468 "***".to_string()
6469 }
6470 }
6471 "hash" => {
6472 use sha2::Digest;
6473 let hash = sha2::Sha256::digest(val.as_bytes());
6474 format!("{:x}", hash)
6475 }
6476 _ => "***".to_string(),
6477 };
6478 Ok(VmValue::String(Arc::from(result.as_str())))
6479 }
6480 BuiltinId::Hash => {
6481 let val = match args.first() {
6482 Some(VmValue::String(s)) => s.to_string(),
6483 _ => return Err(runtime_err("hash: need string")),
6484 };
6485 let algo = match args.get(1) {
6486 Some(VmValue::String(s)) => s.to_string(),
6487 _ => "sha256".to_string(),
6488 };
6489 let result = match algo.as_str() {
6490 "sha256" => {
6491 use sha2::Digest;
6492 format!("{:x}", sha2::Sha256::digest(val.as_bytes()))
6493 }
6494 "sha512" => {
6495 use sha2::Digest;
6496 format!("{:x}", sha2::Sha512::digest(val.as_bytes()))
6497 }
6498 "md5" => {
6499 use md5::Digest;
6500 format!("{:x}", md5::Md5::digest(val.as_bytes()))
6501 }
6502 _ => {
6503 return Err(runtime_err(format!(
6504 "hash: unknown algorithm '{algo}' (use sha256, sha512, or md5)"
6505 )));
6506 }
6507 };
6508 Ok(VmValue::String(Arc::from(result.as_str())))
6509 }
6510
6511 #[cfg(feature = "async-runtime")]
6513 BuiltinId::AsyncReadFile => {
6514 let rt = self.ensure_runtime();
6515 crate::async_runtime::async_read_file_impl(&rt, &args, &self.security_policy)
6516 }
6517 #[cfg(feature = "async-runtime")]
6518 BuiltinId::AsyncWriteFile => {
6519 let rt = self.ensure_runtime();
6520 crate::async_runtime::async_write_file_impl(&rt, &args, &self.security_policy)
6521 }
6522 #[cfg(feature = "async-runtime")]
6523 BuiltinId::AsyncHttpGet => {
6524 let rt = self.ensure_runtime();
6525 crate::async_runtime::async_http_get_impl(&rt, &args, &self.security_policy)
6526 }
6527 #[cfg(feature = "async-runtime")]
6528 BuiltinId::AsyncHttpPost => {
6529 let rt = self.ensure_runtime();
6530 crate::async_runtime::async_http_post_impl(&rt, &args, &self.security_policy)
6531 }
6532 #[cfg(feature = "async-runtime")]
6533 BuiltinId::AsyncSleep => {
6534 let rt = self.ensure_runtime();
6535 crate::async_runtime::async_sleep_impl(&rt, &args)
6536 }
6537 #[cfg(feature = "async-runtime")]
6538 BuiltinId::Select => crate::async_runtime::select_impl(&args),
6539 #[cfg(feature = "async-runtime")]
6540 BuiltinId::RaceAll => crate::async_runtime::race_all_impl(&args),
6541 #[cfg(feature = "async-runtime")]
6542 BuiltinId::AsyncMap => {
6543 let rt = self.ensure_runtime();
6544 let stack_snapshot = self.stack.clone();
6545 crate::async_runtime::async_map_impl(&rt, &args, &self.globals, &stack_snapshot)
6546 }
6547 #[cfg(feature = "async-runtime")]
6548 BuiltinId::AsyncFilter => {
6549 let rt = self.ensure_runtime();
6550 let stack_snapshot = self.stack.clone();
6551 crate::async_runtime::async_filter_impl(&rt, &args, &self.globals, &stack_snapshot)
6552 }
6553
6554 #[cfg(not(feature = "async-runtime"))]
6555 BuiltinId::AsyncReadFile
6556 | BuiltinId::AsyncWriteFile
6557 | BuiltinId::AsyncHttpGet
6558 | BuiltinId::AsyncHttpPost
6559 | BuiltinId::AsyncSleep
6560 | BuiltinId::Select
6561 | BuiltinId::AsyncMap
6562 | BuiltinId::AsyncFilter
6563 | BuiltinId::RaceAll => Err(runtime_err(format!(
6564 "{}: async builtins require the 'async-runtime' feature",
6565 builtin_id.name()
6566 ))),
6567
6568 BuiltinId::IsError => {
6570 if args.is_empty() {
6571 return Err(runtime_err("is_error() expects 1 argument"));
6572 }
6573 let is_err = matches!(&args[0], VmValue::EnumInstance(e) if
6574 &*e.type_name == "DataError" ||
6575 &*e.type_name == "NetworkError" ||
6576 &*e.type_name == "ConnectorError"
6577 );
6578 Ok(VmValue::Bool(is_err))
6579 }
6580 BuiltinId::ErrorType => {
6581 if args.is_empty() {
6582 return Err(runtime_err("error_type() expects 1 argument"));
6583 }
6584 match &args[0] {
6585 VmValue::EnumInstance(e) => Ok(VmValue::String(e.type_name.clone())),
6586 _ => Ok(VmValue::None),
6587 }
6588 }
6589
6590 #[cfg(feature = "gpu")]
6592 BuiltinId::GpuAvailable => Ok(VmValue::Bool(tl_gpu::GpuDevice::is_available())),
6593 #[cfg(not(feature = "gpu"))]
6594 BuiltinId::GpuAvailable => Ok(VmValue::Bool(false)),
6595
6596 #[cfg(feature = "gpu")]
6597 BuiltinId::ToGpu => {
6598 if args.is_empty() {
6599 return Err(runtime_err("to_gpu() expects 1 argument (tensor)"));
6600 }
6601 let gt = self.ensure_gpu_tensor(&args[0])?;
6602 Ok(VmValue::GpuTensor(gt))
6603 }
6604 #[cfg(not(feature = "gpu"))]
6605 BuiltinId::ToGpu => Err(runtime_err(
6606 "GPU operations not available. Build with --features gpu",
6607 )),
6608
6609 #[cfg(feature = "gpu")]
6610 BuiltinId::ToCpu => {
6611 if args.is_empty() {
6612 return Err(runtime_err("to_cpu() expects 1 argument (gpu_tensor)"));
6613 }
6614 match &args[0] {
6615 VmValue::GpuTensor(gt) => {
6616 let cpu = gt.to_cpu().map_err(runtime_err)?;
6617 Ok(VmValue::Tensor(Arc::new(cpu)))
6618 }
6619 _ => Err(runtime_err(format!(
6620 "to_cpu() expects a gpu_tensor, got {}",
6621 args[0].type_name()
6622 ))),
6623 }
6624 }
6625 #[cfg(not(feature = "gpu"))]
6626 BuiltinId::ToCpu => Err(runtime_err(
6627 "GPU operations not available. Build with --features gpu",
6628 )),
6629
6630 #[cfg(feature = "gpu")]
6631 BuiltinId::GpuMatmul => {
6632 if args.len() < 2 {
6633 return Err(runtime_err("gpu_matmul() expects 2 arguments"));
6634 }
6635 let a = self.ensure_gpu_tensor(&args[0])?;
6636 let b = self.ensure_gpu_tensor(&args[1])?;
6637 let ops = self.get_gpu_ops()?;
6638 let result = ops.matmul(&a, &b).map_err(runtime_err)?;
6639 Ok(VmValue::GpuTensor(Arc::new(result)))
6640 }
6641 #[cfg(not(feature = "gpu"))]
6642 BuiltinId::GpuMatmul => Err(runtime_err(
6643 "GPU operations not available. Build with --features gpu",
6644 )),
6645
6646 #[cfg(feature = "gpu")]
6647 BuiltinId::GpuBatchPredict => {
6648 if args.len() < 2 {
6649 return Err(runtime_err("gpu_batch_predict() expects 2-3 arguments"));
6650 }
6651 match (&args[0], &args[1]) {
6652 (VmValue::Model(model), VmValue::Tensor(input)) => {
6653 let batch_size = args.get(2).and_then(|v| match v {
6654 VmValue::Int(n) => Some(*n as usize),
6655 _ => None,
6656 });
6657 let result =
6658 tl_gpu::BatchInference::batch_predict(model, input, batch_size)
6659 .map_err(runtime_err)?;
6660 Ok(VmValue::Tensor(Arc::new(result)))
6661 }
6662 _ => Err(runtime_err(
6663 "gpu_batch_predict() expects (model, tensor, [batch_size])",
6664 )),
6665 }
6666 }
6667 #[cfg(not(feature = "gpu"))]
6668 BuiltinId::GpuBatchPredict => Err(runtime_err(
6669 "GPU operations not available. Build with --features gpu",
6670 )),
6671 #[cfg(feature = "native")]
6673 BuiltinId::Embed => {
6674 if args.is_empty() {
6675 return Err(runtime_err("embed() requires a text argument"));
6676 }
6677 let text = match &args[0] {
6678 VmValue::String(s) => s.to_string(),
6679 _ => return Err(runtime_err("embed() expects a string")),
6680 };
6681 let model = args
6682 .get(1)
6683 .and_then(|v| match v {
6684 VmValue::String(s) => Some(s.to_string()),
6685 _ => None,
6686 })
6687 .unwrap_or_else(|| "text-embedding-3-small".to_string());
6688 let api_key = args
6689 .get(2)
6690 .and_then(|v| match v {
6691 VmValue::String(s) => Some(s.to_string()),
6692 _ => None,
6693 })
6694 .or_else(|| std::env::var("TL_OPENAI_KEY").ok())
6695 .ok_or_else(|| {
6696 runtime_err(
6697 "embed() requires an API key. Set TL_OPENAI_KEY or pass as 3rd arg",
6698 )
6699 })?;
6700 let tensor = tl_ai::embed::embed_api(&text, "openai", &model, &api_key)
6701 .map_err(|e| runtime_err(format!("embed error: {e}")))?;
6702 Ok(VmValue::Tensor(Arc::new(tensor)))
6703 }
6704 #[cfg(not(feature = "native"))]
6705 BuiltinId::Embed => Err(runtime_err("embed() not available in WASM")),
6706 #[cfg(feature = "native")]
6707 BuiltinId::HttpRequest => {
6708 self.check_permission("network")?;
6709 if args.len() < 2 {
6710 return Err(runtime_err(
6711 "http_request(method, url, headers?, body?) expects at least 2 args",
6712 ));
6713 }
6714 let method = match &args[0] {
6715 VmValue::String(s) => s.to_string(),
6716 _ => return Err(runtime_err("http_request() method must be a string")),
6717 };
6718 let url = match &args[1] {
6719 VmValue::String(s) => s.to_string(),
6720 _ => return Err(runtime_err("http_request() url must be a string")),
6721 };
6722 let client = reqwest::blocking::Client::new();
6723 let mut builder = match method.to_uppercase().as_str() {
6724 "GET" => client.get(&url),
6725 "POST" => client.post(&url),
6726 "PUT" => client.put(&url),
6727 "DELETE" => client.delete(&url),
6728 "PATCH" => client.patch(&url),
6729 "HEAD" => client.head(&url),
6730 _ => return Err(runtime_err(format!("Unsupported HTTP method: {method}"))),
6731 };
6732 if let Some(VmValue::Map(headers)) = args.get(2) {
6734 for (key, val) in headers.iter() {
6735 if let VmValue::String(v) = val {
6736 builder = builder.header(key.as_ref(), v.as_ref());
6737 }
6738 }
6739 }
6740 if let Some(VmValue::String(body)) = args.get(3) {
6742 builder = builder.body(body.as_ref().to_string());
6743 }
6744 let resp = builder
6745 .send()
6746 .map_err(|e| runtime_err(format!("HTTP error: {e}")))?;
6747 let status = resp.status().as_u16() as i64;
6748 let body = resp
6749 .text()
6750 .map_err(|e| runtime_err(format!("HTTP response error: {e}")))?;
6751 Ok(VmValue::Map(Box::new(vec![
6752 (Arc::from("status"), VmValue::Int(status)),
6753 (Arc::from("body"), VmValue::String(Arc::from(body.as_str()))),
6754 ])))
6755 }
6756 #[cfg(not(feature = "native"))]
6757 BuiltinId::HttpRequest => Err(runtime_err("http_request() not available in WASM")),
6758 #[cfg(feature = "native")]
6759 BuiltinId::RunAgent => {
6760 self.check_permission("network")?;
6761 if args.len() < 2 {
6762 return Err(runtime_err(
6763 "run_agent(agent, message, [history]) expects at least 2 arguments",
6764 ));
6765 }
6766 let agent_def = match &args[0] {
6767 VmValue::AgentDef(def) => def.clone(),
6768 _ => return Err(runtime_err("run_agent() first arg must be an agent")),
6769 };
6770 let message = match &args[1] {
6771 VmValue::String(s) => s.to_string(),
6772 _ => return Err(runtime_err("run_agent() second arg must be a string")),
6773 };
6774 let history = if args.len() >= 3 {
6776 match &args[2] {
6777 VmValue::List(items) => {
6778 let mut hist = Vec::new();
6779 for item in items.iter() {
6780 if let VmValue::List(pair) = item
6781 && pair.len() >= 2
6782 {
6783 let role = match &pair[0] {
6784 VmValue::String(s) => s.to_string(),
6785 _ => continue,
6786 };
6787 let content = match &pair[1] {
6788 VmValue::String(s) => s.to_string(),
6789 _ => continue,
6790 };
6791 hist.push((role, content));
6792 }
6793 }
6794 Some(hist)
6795 }
6796 _ => None,
6797 }
6798 } else {
6799 None
6800 };
6801 self.exec_agent_loop(&agent_def, &message, history.as_deref())
6802 }
6803 #[cfg(not(feature = "native"))]
6804 BuiltinId::RunAgent => Err(runtime_err("run_agent() not available in WASM")),
6805
6806 #[cfg(feature = "native")]
6808 BuiltinId::StreamAgent => {
6809 self.check_permission("network")?;
6810 if args.len() < 3 {
6811 return Err(runtime_err(
6812 "stream_agent(agent, message, callback) expects 3 arguments",
6813 ));
6814 }
6815 let agent_def = match &args[0] {
6816 VmValue::AgentDef(def) => def.clone(),
6817 _ => return Err(runtime_err("stream_agent() first arg must be an agent")),
6818 };
6819 let message = match &args[1] {
6820 VmValue::String(s) => s.to_string(),
6821 _ => return Err(runtime_err("stream_agent() second arg must be a string")),
6822 };
6823 let callback = args[2].clone();
6824
6825 let model = &agent_def.model;
6826 let system = agent_def.system_prompt.as_deref();
6827 let base_url = agent_def.base_url.as_deref();
6828 let api_key = agent_def.api_key.as_deref();
6829
6830 let messages = vec![serde_json::json!({"role": "user", "content": &message})];
6831 let mut reader = tl_ai::stream_chat(model, system, &messages, base_url, api_key)
6832 .map_err(|e| runtime_err(format!("Stream error: {e}")))?;
6833
6834 let mut full_text = String::new();
6835 loop {
6836 match reader.next_chunk() {
6837 Ok(Some(chunk)) => {
6838 full_text.push_str(&chunk);
6839 let chunk_val = VmValue::String(Arc::from(&*chunk));
6840 let _ = self.call_value(callback.clone(), &[chunk_val]);
6841 }
6842 Ok(None) => break,
6843 Err(e) => return Err(runtime_err(format!("Stream error: {e}"))),
6844 }
6845 }
6846
6847 Ok(VmValue::String(Arc::from(&*full_text)))
6848 }
6849 #[cfg(not(feature = "native"))]
6850 BuiltinId::StreamAgent => Err(runtime_err("stream_agent() not available in WASM")),
6851
6852 #[cfg(feature = "native")]
6854 BuiltinId::Random => {
6855 let mut rng = rand::thread_rng();
6856 let val: f64 = rand::Rng::r#gen(&mut rng);
6857 Ok(VmValue::Float(val))
6858 }
6859 #[cfg(not(feature = "native"))]
6860 BuiltinId::Random => Err(runtime_err("random() not available in WASM")),
6861 #[cfg(feature = "native")]
6862 BuiltinId::RandomInt => {
6863 if args.len() < 2 {
6864 return Err(runtime_err("random_int() expects min and max"));
6865 }
6866 let a = match &args[0] {
6867 VmValue::Int(n) => *n,
6868 _ => return Err(runtime_err("random_int() expects integers")),
6869 };
6870 let b = match &args[1] {
6871 VmValue::Int(n) => *n,
6872 _ => return Err(runtime_err("random_int() expects integers")),
6873 };
6874 if a >= b {
6875 return Err(runtime_err("random_int() requires min < max"));
6876 }
6877 let mut rng = rand::thread_rng();
6878 let val: i64 = rand::Rng::gen_range(&mut rng, a..b);
6879 Ok(VmValue::Int(val))
6880 }
6881 #[cfg(not(feature = "native"))]
6882 BuiltinId::RandomInt => Err(runtime_err("random_int() not available in WASM")),
6883 #[cfg(feature = "native")]
6884 BuiltinId::Sample => {
6885 use rand::seq::SliceRandom;
6886 if args.is_empty() {
6887 return Err(runtime_err("sample() expects a list and count"));
6888 }
6889 let items = match &args[0] {
6890 VmValue::List(items) => items,
6891 _ => return Err(runtime_err("sample() expects a list")),
6892 };
6893 let k = match args.get(1) {
6894 Some(VmValue::Int(n)) => *n as usize,
6895 _ => 1,
6896 };
6897 if k > items.len() {
6898 return Err(runtime_err("sample() count exceeds list length"));
6899 }
6900 let mut rng = rand::thread_rng();
6901 let mut indices: Vec<usize> = (0..items.len()).collect();
6902 indices.partial_shuffle(&mut rng, k);
6903 let result: Vec<VmValue> = indices[..k].iter().map(|&i| items[i].clone()).collect();
6904 if k == 1 && args.get(1).is_none() {
6905 Ok(result.into_iter().next().unwrap_or(VmValue::None))
6906 } else {
6907 Ok(VmValue::List(Box::new(result)))
6908 }
6909 }
6910 #[cfg(not(feature = "native"))]
6911 BuiltinId::Sample => Err(runtime_err("sample() not available in WASM")),
6912
6913 BuiltinId::Exp => {
6915 let x = match args.first() {
6916 Some(VmValue::Float(f)) => *f,
6917 Some(VmValue::Int(n)) => *n as f64,
6918 _ => return Err(runtime_err("exp() expects a number")),
6919 };
6920 Ok(VmValue::Float(x.exp()))
6921 }
6922 BuiltinId::IsNan => {
6923 let result = match args.first() {
6924 Some(VmValue::Float(f)) => f.is_nan(),
6925 _ => false,
6926 };
6927 Ok(VmValue::Bool(result))
6928 }
6929 BuiltinId::IsInfinite => {
6930 let result = match args.first() {
6931 Some(VmValue::Float(f)) => f.is_infinite(),
6932 _ => false,
6933 };
6934 Ok(VmValue::Bool(result))
6935 }
6936 BuiltinId::Sign => match args.first() {
6937 Some(VmValue::Int(n)) => Ok(VmValue::Int(if *n > 0 {
6938 1
6939 } else if *n < 0 {
6940 -1
6941 } else {
6942 0
6943 })),
6944 Some(VmValue::Float(f)) => {
6945 if f.is_nan() {
6946 Ok(VmValue::Float(f64::NAN))
6947 } else if *f > 0.0 {
6948 Ok(VmValue::Int(1))
6949 } else if *f < 0.0 {
6950 Ok(VmValue::Int(-1))
6951 } else {
6952 Ok(VmValue::Int(0))
6953 }
6954 }
6955 _ => Err(runtime_err("sign() expects a number")),
6956 },
6957 #[cfg(feature = "native")]
6959 BuiltinId::AssertTableEq => {
6960 if args.len() < 2 {
6961 return Err(runtime_err("assert_table_eq() expects 2 table arguments"));
6962 }
6963 let t1 = match &args[0] {
6964 VmValue::Table(t) => t,
6965 _ => {
6966 return Err(runtime_err(
6967 "assert_table_eq() first argument must be a table",
6968 ));
6969 }
6970 };
6971 let t2 = match &args[1] {
6972 VmValue::Table(t) => t,
6973 _ => {
6974 return Err(runtime_err(
6975 "assert_table_eq() second argument must be a table",
6976 ));
6977 }
6978 };
6979 if t1.df.schema() != t2.df.schema() {
6981 return Err(runtime_err(format!(
6982 "assert_table_eq: schemas differ\n left: {:?}\n right: {:?}",
6983 t1.df.schema(),
6984 t2.df.schema()
6985 )));
6986 }
6987 let batches1 = self.engine().collect(t1.df.clone()).map_err(runtime_err)?;
6989 let batches2 = self.engine().collect(t2.df.clone()).map_err(runtime_err)?;
6990 let rows1: Vec<String> = batches1
6992 .iter()
6993 .flat_map(|b| {
6994 (0..b.num_rows()).map(move |r| {
6995 (0..b.num_columns())
6996 .map(|c| {
6997 let col = b.column(c);
6998 format!("{:?}", col.slice(r, 1))
6999 })
7000 .collect::<Vec<_>>()
7001 .join(",")
7002 })
7003 })
7004 .collect();
7005 let rows2: Vec<String> = batches2
7006 .iter()
7007 .flat_map(|b| {
7008 (0..b.num_rows()).map(move |r| {
7009 (0..b.num_columns())
7010 .map(|c| {
7011 let col = b.column(c);
7012 format!("{:?}", col.slice(r, 1))
7013 })
7014 .collect::<Vec<_>>()
7015 .join(",")
7016 })
7017 })
7018 .collect();
7019 if rows1.len() != rows2.len() {
7020 return Err(runtime_err(format!(
7021 "assert_table_eq: row count differs ({} vs {})",
7022 rows1.len(),
7023 rows2.len()
7024 )));
7025 }
7026 for (i, (r1, r2)) in rows1.iter().zip(rows2.iter()).enumerate() {
7027 if r1 != r2 {
7028 return Err(runtime_err(format!(
7029 "assert_table_eq: row {} differs\n left: {}\n right: {}",
7030 i, r1, r2
7031 )));
7032 }
7033 }
7034 Ok(VmValue::None)
7035 }
7036 #[cfg(not(feature = "native"))]
7037 BuiltinId::AssertTableEq => Err(runtime_err("assert_table_eq() not available in WASM")),
7038
7039 BuiltinId::Today => {
7041 use chrono::{Datelike, TimeZone};
7042 let now = chrono::Utc::now();
7043 let midnight = chrono::Utc
7044 .with_ymd_and_hms(now.year(), now.month(), now.day(), 0, 0, 0)
7045 .single()
7046 .ok_or_else(|| runtime_err("Failed to compute today"))?;
7047 Ok(VmValue::DateTime(midnight.timestamp_millis()))
7048 }
7049 BuiltinId::DateAdd => {
7050 if args.len() < 3 {
7051 return Err(runtime_err("date_add() expects datetime, amount, unit"));
7052 }
7053 let ms = match &args[0] {
7054 VmValue::DateTime(ms) => *ms,
7055 VmValue::Int(ms) => *ms,
7056 _ => return Err(runtime_err("date_add() first arg must be datetime")),
7057 };
7058 let amount = match &args[1] {
7059 VmValue::Int(n) => *n,
7060 _ => return Err(runtime_err("date_add() amount must be an integer")),
7061 };
7062 let unit = match &args[2] {
7063 VmValue::String(s) => s.as_ref(),
7064 _ => return Err(runtime_err("date_add() unit must be a string")),
7065 };
7066 let offset_ms = match unit {
7067 "second" | "seconds" => amount * 1000,
7068 "minute" | "minutes" => amount * 60 * 1000,
7069 "hour" | "hours" => amount * 3600 * 1000,
7070 "day" | "days" => amount * 86400 * 1000,
7071 "week" | "weeks" => amount * 7 * 86400 * 1000,
7072 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
7073 };
7074 Ok(VmValue::DateTime(ms + offset_ms))
7075 }
7076 BuiltinId::DateDiff => {
7077 if args.len() < 3 {
7078 return Err(runtime_err(
7079 "date_diff() expects datetime1, datetime2, unit",
7080 ));
7081 }
7082 let ms1 = match &args[0] {
7083 VmValue::DateTime(ms) => *ms,
7084 VmValue::Int(ms) => *ms,
7085 _ => return Err(runtime_err("date_diff() args must be datetimes")),
7086 };
7087 let ms2 = match &args[1] {
7088 VmValue::DateTime(ms) => *ms,
7089 VmValue::Int(ms) => *ms,
7090 _ => return Err(runtime_err("date_diff() args must be datetimes")),
7091 };
7092 let unit = match &args[2] {
7093 VmValue::String(s) => s.as_ref(),
7094 _ => return Err(runtime_err("date_diff() unit must be a string")),
7095 };
7096 let diff_ms = ms1 - ms2;
7097 let result = match unit {
7098 "second" | "seconds" => diff_ms / 1000,
7099 "minute" | "minutes" => diff_ms / (60 * 1000),
7100 "hour" | "hours" => diff_ms / (3600 * 1000),
7101 "day" | "days" => diff_ms / (86400 * 1000),
7102 "week" | "weeks" => diff_ms / (7 * 86400 * 1000),
7103 _ => return Err(runtime_err(format!("Unknown time unit: {unit}"))),
7104 };
7105 Ok(VmValue::Int(result))
7106 }
7107 BuiltinId::DateTrunc => {
7108 if args.len() < 2 {
7109 return Err(runtime_err("date_trunc() expects datetime and unit"));
7110 }
7111 let ms = match &args[0] {
7112 VmValue::DateTime(ms) => *ms,
7113 VmValue::Int(ms) => *ms,
7114 _ => return Err(runtime_err("date_trunc() first arg must be datetime")),
7115 };
7116 let unit = match &args[1] {
7117 VmValue::String(s) => s.as_ref(),
7118 _ => return Err(runtime_err("date_trunc() unit must be a string")),
7119 };
7120 use chrono::{Datelike, TimeZone, Timelike};
7121 let secs = ms / 1000;
7122 let dt = chrono::Utc
7123 .timestamp_opt(secs, 0)
7124 .single()
7125 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
7126 let truncated = match unit {
7127 "second" => chrono::Utc
7128 .with_ymd_and_hms(
7129 dt.year(),
7130 dt.month(),
7131 dt.day(),
7132 dt.hour(),
7133 dt.minute(),
7134 dt.second(),
7135 )
7136 .single(),
7137 "minute" => chrono::Utc
7138 .with_ymd_and_hms(
7139 dt.year(),
7140 dt.month(),
7141 dt.day(),
7142 dt.hour(),
7143 dt.minute(),
7144 0,
7145 )
7146 .single(),
7147 "hour" => chrono::Utc
7148 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
7149 .single(),
7150 "day" => chrono::Utc
7151 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
7152 .single(),
7153 "month" => chrono::Utc
7154 .with_ymd_and_hms(dt.year(), dt.month(), 1, 0, 0, 0)
7155 .single(),
7156 "year" => chrono::Utc
7157 .with_ymd_and_hms(dt.year(), 1, 1, 0, 0, 0)
7158 .single(),
7159 _ => return Err(runtime_err(format!("Unknown truncation unit: {unit}"))),
7160 };
7161 Ok(VmValue::DateTime(
7162 truncated
7163 .ok_or_else(|| runtime_err("Invalid truncation"))?
7164 .timestamp_millis(),
7165 ))
7166 }
7167 BuiltinId::DateExtract => {
7168 if args.len() < 2 {
7169 return Err(runtime_err("extract() expects datetime and part"));
7170 }
7171 let ms = match &args[0] {
7172 VmValue::DateTime(ms) => *ms,
7173 VmValue::Int(ms) => *ms,
7174 _ => return Err(runtime_err("extract() first arg must be datetime")),
7175 };
7176 let part = match &args[1] {
7177 VmValue::String(s) => s.as_ref(),
7178 _ => return Err(runtime_err("extract() part must be a string")),
7179 };
7180 use chrono::{Datelike, TimeZone, Timelike};
7181 let secs = ms / 1000;
7182 let dt = chrono::Utc
7183 .timestamp_opt(secs, 0)
7184 .single()
7185 .ok_or_else(|| runtime_err("Invalid timestamp"))?;
7186 let val = match part {
7187 "year" => dt.year() as i64,
7188 "month" => dt.month() as i64,
7189 "day" => dt.day() as i64,
7190 "hour" => dt.hour() as i64,
7191 "minute" => dt.minute() as i64,
7192 "second" => dt.second() as i64,
7193 "weekday" | "dow" => dt.weekday().num_days_from_monday() as i64,
7194 "day_of_year" | "doy" => dt.ordinal() as i64,
7195 _ => return Err(runtime_err(format!("Unknown date part: {part}"))),
7196 };
7197 Ok(VmValue::Int(val))
7198 }
7199
7200 #[cfg(feature = "mcp")]
7202 BuiltinId::McpConnect => {
7203 if args.is_empty() {
7204 return Err(runtime_err(
7205 "mcp_connect expects at least 1 argument: command or URL",
7206 ));
7207 }
7208 let command = match &args[0] {
7209 VmValue::String(s) => s.to_string(),
7210 _ => return Err(runtime_err("mcp_connect: first argument must be a string")),
7211 };
7212
7213 #[cfg(feature = "native")]
7215 let sampling_cb: Option<tl_mcp::SamplingCallback> =
7216 Some(Arc::new(|req: tl_mcp::SamplingRequest| {
7217 let model = req
7218 .model_hint
7219 .as_deref()
7220 .unwrap_or("claude-sonnet-4-20250514");
7221 let messages: Vec<serde_json::Value> = req
7222 .messages
7223 .iter()
7224 .map(|(role, content)| {
7225 serde_json::json!({"role": role, "content": content})
7226 })
7227 .collect();
7228 let response = tl_ai::chat_with_tools(
7229 model,
7230 req.system_prompt.as_deref(),
7231 &messages,
7232 &[], None, None, None, )
7237 .map_err(|e| format!("Sampling LLM error: {e}"))?;
7238 match response {
7239 tl_ai::LlmResponse::Text(text) => Ok(tl_mcp::SamplingResponse {
7240 model: model.to_string(),
7241 content: text,
7242 stop_reason: Some("endTurn".to_string()),
7243 }),
7244 tl_ai::LlmResponse::ToolUse(_) => {
7245 Err("Sampling does not support tool use".to_string())
7246 }
7247 }
7248 }));
7249
7250 #[cfg(not(feature = "native"))]
7251 let sampling_cb: Option<tl_mcp::SamplingCallback> = None;
7252
7253 let client = if command.starts_with("http://") || command.starts_with("https://") {
7255 tl_mcp::McpClient::connect_http_with_sampling(&command, sampling_cb)
7256 .map_err(|e| runtime_err(format!("mcp_connect (HTTP) failed: {e}")))?
7257 } else {
7258 let cmd_args: Vec<String> = args[1..]
7259 .iter()
7260 .map(|a| match a {
7261 VmValue::String(s) => s.to_string(),
7262 other => format!("{}", other),
7263 })
7264 .collect();
7265 tl_mcp::McpClient::connect_with_sampling(
7266 &command,
7267 &cmd_args,
7268 self.security_policy.as_ref(),
7269 sampling_cb,
7270 )
7271 .map_err(|e| runtime_err(format!("mcp_connect failed: {e}")))?
7272 };
7273 Ok(VmValue::McpClient(Arc::new(client)))
7274 }
7275 #[cfg(not(feature = "mcp"))]
7276 BuiltinId::McpConnect => {
7277 Err(runtime_err("MCP not available. Build with --features mcp"))
7278 }
7279
7280 #[cfg(feature = "mcp")]
7281 BuiltinId::McpListTools => {
7282 if args.is_empty() {
7283 return Err(runtime_err("mcp_list_tools expects 1 argument: client"));
7284 }
7285 match &args[0] {
7286 VmValue::McpClient(client) => {
7287 let tools = client
7288 .list_tools()
7289 .map_err(|e| runtime_err(format!("mcp_list_tools failed: {e}")))?;
7290 let tool_values: Vec<VmValue> = tools
7291 .iter()
7292 .map(|tool| {
7293 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7294 pairs.push((
7295 Arc::from("name"),
7296 VmValue::String(Arc::from(tool.name.as_ref())),
7297 ));
7298 if let Some(desc) = &tool.description {
7299 pairs.push((
7300 Arc::from("description"),
7301 VmValue::String(Arc::from(desc.as_ref())),
7302 ));
7303 }
7304 let schema_json = serde_json::to_string(tool.input_schema.as_ref())
7305 .unwrap_or_default();
7306 if !schema_json.is_empty() && schema_json != "{}" {
7307 pairs.push((
7308 Arc::from("input_schema"),
7309 VmValue::String(Arc::from(schema_json.as_str())),
7310 ));
7311 }
7312 VmValue::Map(Box::new(pairs))
7313 })
7314 .collect();
7315 Ok(VmValue::List(Box::new(tool_values)))
7316 }
7317 _ => Err(runtime_err(
7318 "mcp_list_tools: argument must be an mcp_client",
7319 )),
7320 }
7321 }
7322 #[cfg(not(feature = "mcp"))]
7323 BuiltinId::McpListTools => {
7324 Err(runtime_err("MCP not available. Build with --features mcp"))
7325 }
7326
7327 #[cfg(feature = "mcp")]
7328 BuiltinId::McpCallTool => {
7329 if args.len() < 2 {
7330 return Err(runtime_err(
7331 "mcp_call_tool expects 2-3 arguments: client, tool_name, [args]",
7332 ));
7333 }
7334 let client = match &args[0] {
7335 VmValue::McpClient(c) => c.clone(),
7336 _ => {
7337 return Err(runtime_err(
7338 "mcp_call_tool: first argument must be an mcp_client",
7339 ));
7340 }
7341 };
7342 let tool_name = match &args[1] {
7343 VmValue::String(s) => s.to_string(),
7344 _ => return Err(runtime_err("mcp_call_tool: tool_name must be a string")),
7345 };
7346 let arguments = if args.len() > 2 {
7347 vm_value_to_json(&args[2])
7348 } else {
7349 serde_json::Value::Object(serde_json::Map::new())
7350 };
7351 let result = client
7352 .call_tool(&tool_name, arguments)
7353 .map_err(|e| runtime_err(format!("mcp_call_tool failed: {e}")))?;
7354 let mut content_parts: Vec<VmValue> = Vec::new();
7355 for content in &result.content {
7356 if let Some(text) = content.as_text() {
7357 content_parts.push(VmValue::String(Arc::from(text.text.as_str())));
7358 }
7359 }
7360 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7361 if content_parts.len() == 1 {
7362 pairs.push((
7363 Arc::from("content"),
7364 content_parts.into_iter().next().unwrap(),
7365 ));
7366 } else {
7367 pairs.push((Arc::from("content"), VmValue::List(Box::new(content_parts))));
7368 }
7369 pairs.push((
7370 Arc::from("is_error"),
7371 VmValue::Bool(result.is_error.unwrap_or(false)),
7372 ));
7373 Ok(VmValue::Map(Box::new(pairs)))
7374 }
7375 #[cfg(not(feature = "mcp"))]
7376 BuiltinId::McpCallTool => {
7377 Err(runtime_err("MCP not available. Build with --features mcp"))
7378 }
7379
7380 #[cfg(feature = "mcp")]
7381 BuiltinId::McpDisconnect => {
7382 if args.is_empty() {
7383 return Err(runtime_err("mcp_disconnect expects 1 argument: client"));
7384 }
7385 match &args[0] {
7386 VmValue::McpClient(_) => Ok(VmValue::None),
7387 _ => Err(runtime_err(
7388 "mcp_disconnect: argument must be an mcp_client",
7389 )),
7390 }
7391 }
7392 #[cfg(not(feature = "mcp"))]
7393 BuiltinId::McpDisconnect => {
7394 Err(runtime_err("MCP not available. Build with --features mcp"))
7395 }
7396
7397 #[cfg(feature = "mcp")]
7398 BuiltinId::McpPing => {
7399 if args.is_empty() {
7400 return Err(runtime_err("mcp_ping expects 1 argument: client"));
7401 }
7402 match &args[0] {
7403 VmValue::McpClient(client) => {
7404 client
7405 .ping()
7406 .map_err(|e| runtime_err(format!("mcp_ping failed: {e}")))?;
7407 Ok(VmValue::Bool(true))
7408 }
7409 _ => Err(runtime_err("mcp_ping: argument must be an mcp_client")),
7410 }
7411 }
7412 #[cfg(not(feature = "mcp"))]
7413 BuiltinId::McpPing => Err(runtime_err("MCP not available. Build with --features mcp")),
7414
7415 #[cfg(feature = "mcp")]
7416 BuiltinId::McpServerInfo => {
7417 if args.is_empty() {
7418 return Err(runtime_err("mcp_server_info expects 1 argument: client"));
7419 }
7420 match &args[0] {
7421 VmValue::McpClient(client) => match client.server_info() {
7422 Some(info) => {
7423 let pairs: Vec<(Arc<str>, VmValue)> = vec![
7424 (
7425 Arc::from("name"),
7426 VmValue::String(Arc::from(info.server_info.name.as_str())),
7427 ),
7428 (
7429 Arc::from("version"),
7430 VmValue::String(Arc::from(info.server_info.version.as_str())),
7431 ),
7432 ];
7433 Ok(VmValue::Map(Box::new(pairs)))
7434 }
7435 None => Ok(VmValue::None),
7436 },
7437 _ => Err(runtime_err(
7438 "mcp_server_info: argument must be an mcp_client",
7439 )),
7440 }
7441 }
7442 #[cfg(not(feature = "mcp"))]
7443 BuiltinId::McpServerInfo => {
7444 Err(runtime_err("MCP not available. Build with --features mcp"))
7445 }
7446
7447 #[cfg(feature = "mcp")]
7448 BuiltinId::McpServe => {
7449 self.check_permission("network")?;
7450 if args.is_empty() {
7451 return Err(runtime_err(
7452 "mcp_serve expects 1 argument: list of tool definitions",
7453 ));
7454 }
7455 let tool_list = match &args[0] {
7456 VmValue::List(items) => items.as_ref().clone(),
7457 _ => {
7458 return Err(runtime_err(
7459 "mcp_serve: argument must be a list of tool maps",
7460 ));
7461 }
7462 };
7463
7464 let mut channel_tools = Vec::new();
7466 let mut tool_handlers: HashMap<String, VmValue> = HashMap::new();
7467
7468 for item in &tool_list {
7469 let pairs = match item {
7470 VmValue::Map(p) => p.as_ref(),
7471 _ => {
7472 return Err(runtime_err(
7473 "mcp_serve: each tool must be a map with name, description, handler",
7474 ));
7475 }
7476 };
7477 let mut name = String::new();
7478 let mut description = String::new();
7479 let mut handler = None;
7480 let mut input_schema = serde_json::json!({"type": "object"});
7481
7482 for (k, v) in pairs {
7483 match k.as_ref() {
7484 "name" => {
7485 if let VmValue::String(s) = v {
7486 name = s.to_string();
7487 }
7488 }
7489 "description" => {
7490 if let VmValue::String(s) = v {
7491 description = s.to_string();
7492 }
7493 }
7494 "handler" => {
7495 handler = Some(v.clone());
7496 }
7497 "input_schema" | "parameters" => {
7498 if let VmValue::String(s) = v
7499 && let Ok(parsed) =
7500 serde_json::from_str::<serde_json::Value>(s.as_ref())
7501 {
7502 input_schema = parsed;
7503 }
7504 }
7505 _ => {}
7506 }
7507 }
7508
7509 if name.is_empty() {
7510 return Err(runtime_err("mcp_serve: tool missing 'name'"));
7511 }
7512 if let Some(h) = handler {
7513 tool_handlers.insert(name.clone(), h);
7514 }
7515
7516 channel_tools.push(tl_mcp::server::ChannelToolDef {
7517 name,
7518 description,
7519 input_schema,
7520 });
7521 }
7522
7523 let (builder, rx) = tl_mcp::server::TlServerHandler::builder()
7525 .name("tl-mcp-server")
7526 .version("1.0.0")
7527 .channel_tools(channel_tools);
7528 let server_handler = builder.build();
7529
7530 let _server_handle = tl_mcp::server::serve_stdio_background(server_handler);
7532
7533 while let Ok(req) = rx.recv() {
7535 let result = if let Some(func) = tool_handlers.get(&req.tool_name) {
7536 let call_args = self.json_to_vm_args(&req.arguments);
7538 match self.call_value(func.clone(), &call_args) {
7539 Ok(val) => {
7540 Ok(serde_json::json!(format!("{val}")))
7542 }
7543 Err(e) => Err(format!("{e}")),
7544 }
7545 } else {
7546 Err(format!("Unknown tool: {}", req.tool_name))
7547 };
7548 let _ = req.response_tx.send(result);
7549 }
7550
7551 Ok(VmValue::None)
7552 }
7553 #[cfg(not(feature = "mcp"))]
7554 BuiltinId::McpServe => Err(runtime_err("MCP not available. Build with --features mcp")),
7555
7556 #[cfg(feature = "mcp")]
7558 BuiltinId::McpListResources => {
7559 if args.is_empty() {
7560 return Err(runtime_err("mcp_list_resources expects 1 argument: client"));
7561 }
7562 match &args[0] {
7563 VmValue::McpClient(client) => {
7564 let resources = client
7565 .list_resources()
7566 .map_err(|e| runtime_err(format!("mcp_list_resources failed: {e}")))?;
7567 let vals: Vec<VmValue> = resources
7568 .iter()
7569 .map(|r| {
7570 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7571 pairs.push((
7572 Arc::from("uri"),
7573 VmValue::String(Arc::from(r.uri.as_str())),
7574 ));
7575 pairs.push((
7576 Arc::from("name"),
7577 VmValue::String(Arc::from(r.name.as_str())),
7578 ));
7579 if let Some(desc) = &r.description {
7580 pairs.push((
7581 Arc::from("description"),
7582 VmValue::String(Arc::from(desc.as_str())),
7583 ));
7584 }
7585 if let Some(mime) = &r.mime_type {
7586 pairs.push((
7587 Arc::from("mime_type"),
7588 VmValue::String(Arc::from(mime.as_str())),
7589 ));
7590 }
7591 VmValue::Map(Box::new(pairs))
7592 })
7593 .collect();
7594 Ok(VmValue::List(Box::new(vals)))
7595 }
7596 _ => Err(runtime_err(
7597 "mcp_list_resources: argument must be an mcp_client",
7598 )),
7599 }
7600 }
7601 #[cfg(not(feature = "mcp"))]
7602 BuiltinId::McpListResources => {
7603 Err(runtime_err("MCP not available. Build with --features mcp"))
7604 }
7605
7606 #[cfg(feature = "mcp")]
7607 BuiltinId::McpReadResource => {
7608 if args.len() < 2 {
7609 return Err(runtime_err(
7610 "mcp_read_resource expects 2 arguments: client, uri",
7611 ));
7612 }
7613 let client = match &args[0] {
7614 VmValue::McpClient(c) => c.clone(),
7615 _ => {
7616 return Err(runtime_err(
7617 "mcp_read_resource: first argument must be an mcp_client",
7618 ));
7619 }
7620 };
7621 let uri = match &args[1] {
7622 VmValue::String(s) => s.to_string(),
7623 _ => return Err(runtime_err("mcp_read_resource: uri must be a string")),
7624 };
7625 let result = client
7626 .read_resource(&uri)
7627 .map_err(|e| runtime_err(format!("mcp_read_resource failed: {e}")))?;
7628 let contents: Vec<VmValue> = result
7630 .contents
7631 .iter()
7632 .map(|content| {
7633 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7634 let json = serde_json::to_value(content).unwrap_or_default();
7635 if let Some(uri_s) = json.get("uri").and_then(|v| v.as_str()) {
7636 pairs.push((Arc::from("uri"), VmValue::String(Arc::from(uri_s))));
7637 }
7638 if let Some(mime) = json.get("mimeType").and_then(|v| v.as_str()) {
7639 pairs.push((Arc::from("mime_type"), VmValue::String(Arc::from(mime))));
7640 }
7641 if let Some(text) = json.get("text").and_then(|v| v.as_str()) {
7642 pairs.push((Arc::from("text"), VmValue::String(Arc::from(text))));
7643 }
7644 if let Some(blob) = json.get("blob").and_then(|v| v.as_str()) {
7645 pairs.push((Arc::from("blob"), VmValue::String(Arc::from(blob))));
7646 }
7647 VmValue::Map(Box::new(pairs))
7648 })
7649 .collect();
7650 if contents.len() == 1 {
7651 Ok(contents.into_iter().next().unwrap())
7652 } else {
7653 Ok(VmValue::List(Box::new(contents)))
7654 }
7655 }
7656 #[cfg(not(feature = "mcp"))]
7657 BuiltinId::McpReadResource => {
7658 Err(runtime_err("MCP not available. Build with --features mcp"))
7659 }
7660
7661 #[cfg(feature = "mcp")]
7662 BuiltinId::McpListPrompts => {
7663 if args.is_empty() {
7664 return Err(runtime_err("mcp_list_prompts expects 1 argument: client"));
7665 }
7666 match &args[0] {
7667 VmValue::McpClient(client) => {
7668 let prompts = client
7669 .list_prompts()
7670 .map_err(|e| runtime_err(format!("mcp_list_prompts failed: {e}")))?;
7671 let vals: Vec<VmValue> = prompts
7672 .iter()
7673 .map(|p| {
7674 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7675 pairs.push((
7676 Arc::from("name"),
7677 VmValue::String(Arc::from(p.name.as_str())),
7678 ));
7679 if let Some(desc) = &p.description {
7680 pairs.push((
7681 Arc::from("description"),
7682 VmValue::String(Arc::from(desc.as_str())),
7683 ));
7684 }
7685 if let Some(prompt_args) = &p.arguments {
7686 let arg_vals: Vec<VmValue> = prompt_args
7687 .iter()
7688 .map(|a| {
7689 let mut arg_pairs: Vec<(Arc<str>, VmValue)> =
7690 Vec::new();
7691 arg_pairs.push((
7692 Arc::from("name"),
7693 VmValue::String(Arc::from(a.name.as_str())),
7694 ));
7695 if let Some(desc) = &a.description {
7696 arg_pairs.push((
7697 Arc::from("description"),
7698 VmValue::String(Arc::from(desc.as_str())),
7699 ));
7700 }
7701 arg_pairs.push((
7702 Arc::from("required"),
7703 VmValue::Bool(a.required.unwrap_or(false)),
7704 ));
7705 VmValue::Map(Box::new(arg_pairs))
7706 })
7707 .collect();
7708 pairs.push((
7709 Arc::from("arguments"),
7710 VmValue::List(Box::new(arg_vals)),
7711 ));
7712 }
7713 VmValue::Map(Box::new(pairs))
7714 })
7715 .collect();
7716 Ok(VmValue::List(Box::new(vals)))
7717 }
7718 _ => Err(runtime_err(
7719 "mcp_list_prompts: argument must be an mcp_client",
7720 )),
7721 }
7722 }
7723 #[cfg(not(feature = "mcp"))]
7724 BuiltinId::McpListPrompts => {
7725 Err(runtime_err("MCP not available. Build with --features mcp"))
7726 }
7727
7728 #[cfg(feature = "mcp")]
7729 BuiltinId::McpGetPrompt => {
7730 if args.len() < 2 {
7731 return Err(runtime_err(
7732 "mcp_get_prompt expects 2-3 arguments: client, name, [args]",
7733 ));
7734 }
7735 let client = match &args[0] {
7736 VmValue::McpClient(c) => c.clone(),
7737 _ => {
7738 return Err(runtime_err(
7739 "mcp_get_prompt: first argument must be an mcp_client",
7740 ));
7741 }
7742 };
7743 let name = match &args[1] {
7744 VmValue::String(s) => s.to_string(),
7745 _ => return Err(runtime_err("mcp_get_prompt: name must be a string")),
7746 };
7747 let prompt_args = if args.len() > 2 {
7748 let json = vm_value_to_json(&args[2]);
7749 json.as_object().cloned()
7750 } else {
7751 None
7752 };
7753 let result = client
7754 .get_prompt(&name, prompt_args)
7755 .map_err(|e| runtime_err(format!("mcp_get_prompt failed: {e}")))?;
7756 let mut pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7757 if let Some(desc) = &result.description {
7758 pairs.push((
7759 Arc::from("description"),
7760 VmValue::String(Arc::from(desc.as_str())),
7761 ));
7762 }
7763 let messages: Vec<VmValue> = result
7765 .messages
7766 .iter()
7767 .map(|m| {
7768 let mut msg_pairs: Vec<(Arc<str>, VmValue)> = Vec::new();
7769 let msg_json = serde_json::to_value(m).unwrap_or_default();
7770 if let Some(role) = msg_json.get("role").and_then(|v| v.as_str()) {
7772 msg_pairs.push((Arc::from("role"), VmValue::String(Arc::from(role))));
7773 }
7774 if let Some(content) = msg_json.get("content") {
7776 if let Some(text) = content.get("text").and_then(|v| v.as_str()) {
7777 msg_pairs
7778 .push((Arc::from("content"), VmValue::String(Arc::from(text))));
7779 } else {
7780 let content_str = content.to_string();
7781 msg_pairs.push((
7782 Arc::from("content"),
7783 VmValue::String(Arc::from(content_str.as_str())),
7784 ));
7785 }
7786 }
7787 VmValue::Map(Box::new(msg_pairs))
7788 })
7789 .collect();
7790 pairs.push((Arc::from("messages"), VmValue::List(Box::new(messages))));
7791 Ok(VmValue::Map(Box::new(pairs)))
7792 }
7793 #[cfg(not(feature = "mcp"))]
7794 BuiltinId::McpGetPrompt => {
7795 Err(runtime_err("MCP not available. Build with --features mcp"))
7796 }
7797 }
7798 }
7799
7800 fn vmvalue_to_f64_list(&self, val: &VmValue) -> Result<Vec<f64>, TlError> {
7803 match val {
7804 VmValue::List(items) => items
7805 .iter()
7806 .map(|item| match item {
7807 VmValue::Int(n) => Ok(*n as f64),
7808 VmValue::Float(f) => Ok(*f),
7809 _ => Err(runtime_err("Expected number in list")),
7810 })
7811 .collect(),
7812 VmValue::Int(n) => Ok(vec![*n as f64]),
7813 VmValue::Float(f) => Ok(vec![*f]),
7814 _ => Err(runtime_err("Expected a list of numbers")),
7815 }
7816 }
7817
7818 fn vmvalue_to_usize_list(&self, val: &VmValue) -> Result<Vec<usize>, TlError> {
7819 match val {
7820 VmValue::List(items) => items
7821 .iter()
7822 .map(|item| match item {
7823 VmValue::Int(n) => Ok(*n as usize),
7824 _ => Err(runtime_err("Expected integer in shape list")),
7825 })
7826 .collect(),
7827 _ => Err(runtime_err("Expected a list of integers for shape")),
7828 }
7829 }
7830
7831 #[cfg(feature = "native")]
7832 fn handle_train(
7833 &mut self,
7834 frame_idx: usize,
7835 algo_const: u8,
7836 config_const: u8,
7837 ) -> Result<VmValue, TlError> {
7838 let frame = &self.frames[frame_idx];
7839 let algorithm = match &frame.prototype.constants[algo_const as usize] {
7840 Constant::String(s) => s.to_string(),
7841 _ => return Err(runtime_err("Expected string constant for algorithm")),
7842 };
7843 let config_args = match &frame.prototype.constants[config_const as usize] {
7844 Constant::AstExprList(args) => args.clone(),
7845 _ => return Err(runtime_err("Expected AST expr list for train config")),
7846 };
7847
7848 let mut data_val = None;
7850 let mut target_name = None;
7851 let mut feature_names: Vec<String> = Vec::new();
7852 let mut hyperparams: std::collections::HashMap<String, f64> =
7853 std::collections::HashMap::new();
7854
7855 for arg in &config_args {
7856 if let AstExpr::NamedArg { name, value } = arg {
7857 match name.as_str() {
7858 "data" => {
7859 data_val = Some(self.eval_ast_to_vm(value)?);
7860 }
7861 "target" => {
7862 if let AstExpr::String(s) = value.as_ref() {
7863 target_name = Some(s.clone());
7864 }
7865 }
7866 "features" => {
7867 if let AstExpr::List(items) = value.as_ref() {
7868 for item in items {
7869 if let AstExpr::String(s) = item {
7870 feature_names.push(s.clone());
7871 }
7872 }
7873 }
7874 }
7875 other => match value.as_ref() {
7879 AstExpr::Int(n) => {
7880 hyperparams.insert(other.to_string(), *n as f64);
7881 }
7882 AstExpr::Float(f) => {
7883 hyperparams.insert(other.to_string(), *f);
7884 }
7885 _ => {}
7886 },
7887 }
7888 }
7889 }
7890
7891 let table = match data_val {
7893 Some(VmValue::Table(t)) => t,
7894 _ => return Err(runtime_err("train: data must be a table")),
7895 };
7896 let target = target_name.ok_or_else(|| runtime_err("train: target is required"))?;
7897
7898 let batches = self.engine().collect(table.df).map_err(runtime_err)?;
7900 if batches.is_empty() {
7901 return Err(runtime_err("train: empty dataset"));
7902 }
7903
7904 let schema = batches[0].schema();
7906 if feature_names.is_empty() {
7907 for field in schema.fields() {
7908 if field.name() != &target {
7909 feature_names.push(field.name().clone());
7910 }
7911 }
7912 }
7913 let n_features = feature_names.len();
7914
7915 let feat_idx: Vec<usize> = feature_names
7917 .iter()
7918 .map(|c| {
7919 schema
7920 .index_of(c)
7921 .map_err(|_| runtime_err(format!("Column not found: {c}")))
7922 })
7923 .collect::<Result<_, _>>()?;
7924 let target_idx = schema
7925 .index_of(&target)
7926 .map_err(|_| runtime_err(format!("Target column not found: {target}")))?;
7927
7928 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
7933 if total_rows == 0 {
7934 return Err(runtime_err("train: empty dataset"));
7935 }
7936 let mut row_major = Vec::with_capacity(total_rows * n_features);
7937 let mut target_data = Vec::with_capacity(total_rows);
7938 for batch in &batches {
7939 let nb = batch.num_rows();
7940 let mut cols: Vec<Vec<f64>> = Vec::with_capacity(n_features);
7941 for &ci in &feat_idx {
7942 let mut tmp = Vec::with_capacity(nb);
7943 Self::extract_f64_column(batch.column(ci), &mut tmp)?;
7944 cols.push(tmp);
7945 }
7946 for r in 0..nb {
7947 for col in &cols {
7948 row_major.push(col[r]);
7949 }
7950 }
7951 Self::extract_f64_column(batch.column(target_idx), &mut target_data)?;
7952 }
7953
7954 let mut order: Vec<usize> = (0..total_rows).collect();
7957 order.sort_by(|&a, &b| {
7958 let ra = &row_major[a * n_features..(a + 1) * n_features];
7959 let rb = &row_major[b * n_features..(b + 1) * n_features];
7960 ra.iter()
7961 .zip(rb)
7962 .map(|(x, y)| x.total_cmp(y))
7963 .find(|o| o.is_ne())
7964 .unwrap_or_else(|| target_data[a].total_cmp(&target_data[b]))
7965 });
7966 let mut sorted_feats = Vec::with_capacity(total_rows * n_features);
7967 let mut sorted_target = Vec::with_capacity(total_rows);
7968 for &i in &order {
7969 sorted_feats.extend_from_slice(&row_major[i * n_features..(i + 1) * n_features]);
7970 sorted_target.push(target_data[i]);
7971 }
7972
7973 let n_rows = total_rows;
7974 let features_tensor = tl_ai::TlTensor::from_vec(sorted_feats, &[n_rows, n_features])
7975 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7976 let target_tensor = tl_ai::TlTensor::from_vec(sorted_target, &[n_rows])
7977 .map_err(|e| runtime_err(format!("Shape error: {e}")))?;
7978
7979 let config = tl_ai::TrainConfig {
7980 features: features_tensor,
7981 target: target_tensor,
7982 feature_names: feature_names.clone(),
7983 target_name: target.clone(),
7984 model_name: algorithm.clone(),
7985 split_ratio: 0.8,
7986 hyperparams,
7987 };
7988
7989 let model = tl_ai::train(&algorithm, &config)
7990 .map_err(|e| runtime_err(format!("Training failed: {e}")))?;
7991
7992 Ok(VmValue::Model(Arc::new(model)))
7993 }
7994
7995 #[cfg(feature = "native")]
7996 fn extract_f64_column(
7997 col: &std::sync::Arc<dyn tl_data::datafusion::arrow::array::Array>,
7998 out: &mut Vec<f64>,
7999 ) -> Result<(), TlError> {
8000 use tl_data::datafusion::arrow::array::{
8001 Array, Float32Array, Float64Array, Int32Array, Int64Array,
8002 };
8003 let len = col.len();
8004 if let Some(arr) = col.as_any().downcast_ref::<Float64Array>() {
8005 for i in 0..len {
8006 out.push(if arr.is_null(i) { 0.0 } else { arr.value(i) });
8007 }
8008 } else if let Some(arr) = col.as_any().downcast_ref::<Int64Array>() {
8009 for i in 0..len {
8010 out.push(if arr.is_null(i) {
8011 0.0
8012 } else {
8013 arr.value(i) as f64
8014 });
8015 }
8016 } else if let Some(arr) = col.as_any().downcast_ref::<Float32Array>() {
8017 for i in 0..len {
8018 out.push(if arr.is_null(i) {
8019 0.0
8020 } else {
8021 arr.value(i) as f64
8022 });
8023 }
8024 } else if let Some(arr) = col.as_any().downcast_ref::<Int32Array>() {
8025 for i in 0..len {
8026 out.push(if arr.is_null(i) {
8027 0.0
8028 } else {
8029 arr.value(i) as f64
8030 });
8031 }
8032 } else {
8033 return Err(runtime_err(
8034 "Column must be numeric (int32, int64, float32, float64)",
8035 ));
8036 }
8037 Ok(())
8038 }
8039
8040 #[cfg(feature = "native")]
8041 fn handle_pipeline_exec(
8042 &mut self,
8043 frame_idx: usize,
8044 name_const: u8,
8045 config_const: u8,
8046 ) -> Result<VmValue, TlError> {
8047 let frame = &self.frames[frame_idx];
8048 let name = match &frame.prototype.constants[name_const as usize] {
8049 Constant::String(s) => s.to_string(),
8050 _ => return Err(runtime_err("Expected string constant for pipeline name")),
8051 };
8052
8053 let mut schedule = None;
8054 let mut timeout_ms = None;
8055 let mut retries = 0u32;
8056
8057 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8058 for arg in args {
8059 if let AstExpr::NamedArg { name: key, value } = arg {
8060 match key.as_str() {
8061 "schedule" => {
8062 if let AstExpr::String(s) = value.as_ref() {
8063 schedule = Some(s.clone());
8064 }
8065 }
8066 "timeout" => {
8067 if let AstExpr::String(s) = value.as_ref() {
8068 timeout_ms = tl_stream::parse_duration(s).ok();
8069 }
8070 }
8071 "retries" => {
8072 if let AstExpr::Int(n) = value.as_ref() {
8073 retries = *n as u32;
8074 }
8075 }
8076 _ => {}
8077 }
8078 }
8079 }
8080 }
8081
8082 let def = tl_stream::PipelineDef {
8083 name,
8084 schedule,
8085 timeout_ms,
8086 retries,
8087 };
8088
8089 self.output
8090 .push(format!("Pipeline '{}': success", def.name));
8091 Ok(VmValue::PipelineDef(Arc::new(def)))
8092 }
8093
8094 #[cfg(feature = "native")]
8095 fn handle_stream_exec(
8096 &mut self,
8097 frame_idx: usize,
8098 config_const: u8,
8099 ) -> Result<VmValue, TlError> {
8100 let frame = &self.frames[frame_idx];
8101 let config_args = match &frame.prototype.constants[config_const as usize] {
8102 Constant::AstExprList(args) => args.clone(),
8103 _ => return Err(runtime_err("Expected AST expr list for stream config")),
8104 };
8105
8106 let mut name = String::new();
8107 let mut window = None;
8108 let mut watermark_ms = None;
8109
8110 for arg in &config_args {
8111 if let AstExpr::NamedArg { name: key, value } = arg {
8112 match key.as_str() {
8113 "name" => {
8114 if let AstExpr::String(s) = value.as_ref() {
8115 name = s.clone();
8116 }
8117 }
8118 "window" => {
8119 if let AstExpr::String(s) = value.as_ref() {
8120 window = Self::parse_window_type(s);
8121 }
8122 }
8123 "watermark" => {
8124 if let AstExpr::String(s) = value.as_ref() {
8125 watermark_ms = tl_stream::parse_duration(s).ok();
8126 }
8127 }
8128 _ => {}
8129 }
8130 }
8131 }
8132
8133 let def = tl_stream::StreamDef {
8134 name: name.clone(),
8135 window,
8136 watermark_ms,
8137 };
8138
8139 self.output.push(format!("Stream '{}' declared", name));
8140 Ok(VmValue::StreamDef(Arc::new(def)))
8141 }
8142
8143 #[cfg(feature = "native")]
8144 fn handle_agent_exec(
8145 &mut self,
8146 frame_idx: usize,
8147 name_const: u8,
8148 config_const: u8,
8149 ) -> Result<VmValue, TlError> {
8150 let frame = &self.frames[frame_idx];
8151 let name = match &frame.prototype.constants[name_const as usize] {
8152 Constant::String(s) => s.to_string(),
8153 _ => return Err(runtime_err("Expected string constant for agent name")),
8154 };
8155
8156 let mut model = String::new();
8157 let mut system_prompt = None;
8158 let mut max_turns = 10u32;
8159 let mut temperature = None;
8160 let mut max_tokens = None;
8161 let mut base_url = None;
8162 let mut api_key = None;
8163 let mut output_format = None;
8164 let mut tools = Vec::new();
8165 #[cfg(feature = "mcp")]
8166 let mut mcp_clients: Vec<Arc<tl_mcp::McpClient>> = Vec::new();
8167
8168 if let Constant::AstExprList(args) = &frame.prototype.constants[config_const as usize] {
8169 for arg in args {
8170 if let AstExpr::NamedArg { name: key, value } = arg {
8171 if let Some(tool_name) = key.strip_prefix("tool:") {
8172 let (desc, params) = Self::extract_tool_from_ast(value);
8174 tools.push(tl_stream::AgentTool {
8175 name: tool_name.to_string(),
8176 description: desc,
8177 parameters: params,
8178 });
8179 } else if key.starts_with("mcp_server:") {
8180 #[cfg(feature = "mcp")]
8182 if let AstExpr::Ident(var_name) = value.as_ref()
8183 && let Some(VmValue::McpClient(client)) = self.globals.get(var_name)
8184 {
8185 mcp_clients.push(client.clone());
8186 }
8187 } else {
8188 match key.as_str() {
8189 "model" => {
8190 if let AstExpr::String(s) = value.as_ref() {
8191 model = s.clone();
8192 }
8193 }
8194 "system" => {
8195 if let AstExpr::String(s) = value.as_ref() {
8196 system_prompt = Some(s.clone());
8197 }
8198 }
8199 "max_turns" => {
8200 if let AstExpr::Int(n) = value.as_ref() {
8201 max_turns = *n as u32;
8202 }
8203 }
8204 "temperature" => {
8205 if let AstExpr::Float(f) = value.as_ref() {
8206 temperature = Some(*f);
8207 }
8208 }
8209 "max_tokens" => {
8210 if let AstExpr::Int(n) = value.as_ref() {
8211 max_tokens = Some(*n as u32);
8212 }
8213 }
8214 "base_url" => {
8215 if let AstExpr::String(s) = value.as_ref() {
8216 base_url = Some(s.clone());
8217 }
8218 }
8219 "api_key" => {
8220 if let AstExpr::String(s) = value.as_ref() {
8221 api_key = Some(s.clone());
8222 }
8223 }
8224 "output_format" => {
8225 if let AstExpr::String(s) = value.as_ref() {
8226 output_format = Some(s.clone());
8227 }
8228 }
8229 _ => {}
8230 }
8231 }
8232 }
8233 }
8234 }
8235
8236 let def = tl_stream::AgentDef {
8237 name: name.clone(),
8238 model,
8239 system_prompt,
8240 tools,
8241 max_turns,
8242 temperature,
8243 max_tokens,
8244 base_url,
8245 api_key,
8246 output_format,
8247 };
8248
8249 #[cfg(feature = "mcp")]
8251 if !mcp_clients.is_empty() {
8252 self.mcp_agent_clients.insert(name.clone(), mcp_clients);
8253 }
8254
8255 Ok(VmValue::AgentDef(Arc::new(def)))
8256 }
8257
8258 #[cfg(feature = "native")]
8259 fn extract_tool_from_ast(expr: &AstExpr) -> (String, serde_json::Value) {
8260 let mut desc = String::new();
8261 let mut params = serde_json::Value::Object(serde_json::Map::new());
8262 if let AstExpr::Map(pairs) = expr {
8263 for (key_expr, val_expr) in pairs {
8264 if let AstExpr::Ident(key) | AstExpr::String(key) = key_expr {
8265 match key.as_str() {
8266 "description" => {
8267 if let AstExpr::String(s) = val_expr {
8268 desc = s.clone();
8269 }
8270 }
8271 "parameters" => {
8272 params = Self::ast_to_json(val_expr);
8273 }
8274 _ => {}
8275 }
8276 }
8277 }
8278 }
8279 (desc, params)
8280 }
8281
8282 #[cfg(feature = "native")]
8283 fn ast_to_json(expr: &AstExpr) -> serde_json::Value {
8284 match expr {
8285 AstExpr::String(s) => serde_json::Value::String(s.clone()),
8286 AstExpr::Int(n) => serde_json::json!(*n),
8287 AstExpr::Float(f) => serde_json::json!(*f),
8288 AstExpr::Bool(b) => serde_json::Value::Bool(*b),
8289 AstExpr::None => serde_json::Value::Null,
8290 AstExpr::List(items) => {
8291 serde_json::Value::Array(items.iter().map(Self::ast_to_json).collect())
8292 }
8293 AstExpr::Map(pairs) => {
8294 let mut map = serde_json::Map::new();
8295 for (k, v) in pairs {
8296 let key = match k {
8297 AstExpr::String(s) | AstExpr::Ident(s) => s.clone(),
8298 _ => format!("{k:?}"),
8299 };
8300 map.insert(key, Self::ast_to_json(v));
8301 }
8302 serde_json::Value::Object(map)
8303 }
8304 _ => serde_json::Value::Null,
8305 }
8306 }
8307
8308 #[cfg(feature = "native")]
8309 fn exec_agent_loop(
8310 &mut self,
8311 agent_def: &tl_stream::AgentDef,
8312 user_message: &str,
8313 history: Option<&[(String, String)]>,
8314 ) -> Result<VmValue, TlError> {
8315 use tl_ai::{LlmResponse, chat_with_tools, format_tool_result_messages};
8316
8317 let model = &agent_def.model;
8318 let system = agent_def.system_prompt.as_deref();
8319 let base_url = agent_def.base_url.as_deref();
8320 let api_key = agent_def.api_key.as_deref();
8321
8322 let provider = if model.starts_with("claude") {
8323 "anthropic"
8324 } else {
8325 "openai"
8326 };
8327
8328 #[allow(unused_mut)]
8330 let mut tools_json: Vec<serde_json::Value> = agent_def
8331 .tools
8332 .iter()
8333 .map(|t| {
8334 serde_json::json!({
8335 "type": "function",
8336 "function": {
8337 "name": t.name,
8338 "description": t.description,
8339 "parameters": t.parameters
8340 }
8341 })
8342 })
8343 .collect();
8344
8345 #[cfg(feature = "mcp")]
8347 let mcp_clients = self
8348 .mcp_agent_clients
8349 .get(&agent_def.name)
8350 .cloned()
8351 .unwrap_or_default();
8352 #[cfg(feature = "mcp")]
8353 let mcp_tool_dispatch: std::collections::HashMap<String, usize> = {
8354 let mut dispatch = std::collections::HashMap::new();
8355 for (client_idx, client) in mcp_clients.iter().enumerate() {
8356 if let Ok(mcp_tools) = client.list_tools() {
8357 for tool in mcp_tools {
8358 let tool_name = tool.name.to_string();
8359 tools_json.push(serde_json::json!({
8360 "type": "function",
8361 "function": {
8362 "name": &tool_name,
8363 "description": tool.description.as_deref().unwrap_or(""),
8364 "parameters": serde_json::Value::Object((*tool.input_schema).clone())
8365 }
8366 }));
8367 dispatch.insert(tool_name, client_idx);
8368 }
8369 }
8370 }
8371 dispatch
8372 };
8373
8374 let mut messages: Vec<serde_json::Value> = Vec::new();
8376 if let Some(hist) = history {
8377 for (role, content) in hist {
8378 messages.push(serde_json::json!({"role": role, "content": content}));
8379 }
8380 }
8381 messages.push(serde_json::json!({
8383 "role": "user",
8384 "content": user_message
8385 }));
8386
8387 for turn in 0..agent_def.max_turns {
8388 let response = chat_with_tools(
8389 model,
8390 system,
8391 &messages,
8392 &tools_json,
8393 base_url,
8394 api_key,
8395 agent_def.output_format.as_deref(),
8396 )
8397 .map_err(|e| runtime_err(format!("Agent LLM error: {e}")))?;
8398
8399 match response {
8400 LlmResponse::Text(text) => {
8401 messages.push(serde_json::json!({"role": "assistant", "content": &text}));
8403
8404 let history_list: Vec<VmValue> = messages
8406 .iter()
8407 .filter_map(|m| {
8408 let role = m["role"].as_str()?;
8409 let content = m["content"].as_str()?;
8410 Some(VmValue::List(Box::new(vec![
8411 VmValue::String(Arc::from(role)),
8412 VmValue::String(Arc::from(content)),
8413 ])))
8414 })
8415 .collect();
8416
8417 let result = VmValue::Map(Box::new(vec![
8419 (
8420 Arc::from("response"),
8421 VmValue::String(Arc::from(text.as_str())),
8422 ),
8423 (Arc::from("turns"), VmValue::Int(turn as i64 + 1)),
8424 (Arc::from("history"), VmValue::List(Box::new(history_list))),
8425 ]));
8426
8427 let hook_name = format!("__agent_{}_on_complete__", agent_def.name);
8429 if let Some(hook) = self.globals.get(&hook_name).cloned() {
8430 let _ = self.call_value(hook, std::slice::from_ref(&result));
8431 }
8432
8433 return Ok(result);
8434 }
8435 LlmResponse::ToolUse(tool_calls) => {
8436 let tc_json: Vec<serde_json::Value> = tool_calls
8438 .iter()
8439 .map(|tc| {
8440 serde_json::json!({
8441 "id": tc.id,
8442 "type": "function",
8443 "function": {
8444 "name": tc.name,
8445 "arguments": serde_json::to_string(&tc.input).unwrap_or_default()
8446 }
8447 })
8448 })
8449 .collect();
8450 messages.push(serde_json::json!({
8451 "role": "assistant",
8452 "tool_calls": tc_json
8453 }));
8454
8455 #[allow(unused_mut)]
8457 let mut declared: Vec<String> =
8458 agent_def.tools.iter().map(|t| t.name.clone()).collect();
8459 #[cfg(feature = "mcp")]
8460 {
8461 for name in mcp_tool_dispatch.keys() {
8462 declared.push(name.clone());
8463 }
8464 }
8465
8466 let mut results: Vec<(String, String)> = Vec::new();
8468 for tc in &tool_calls {
8469 if !declared.iter().any(|d| d == &tc.name) {
8470 results.push((
8471 tc.name.clone(),
8472 format!("Error: '{}' not in declared tools", tc.name),
8473 ));
8474 continue;
8475 }
8476
8477 let result_str;
8479 #[cfg(feature = "mcp")]
8480 {
8481 if let Some(&client_idx) = mcp_tool_dispatch.get(tc.name.as_str()) {
8482 let mcp_result = mcp_clients[client_idx]
8483 .call_tool(&tc.name, tc.input.clone())
8484 .map_err(|e| runtime_err(format!("MCP tool error: {e}")))?;
8485 result_str = mcp_result
8486 .content
8487 .iter()
8488 .filter_map(|c| c.raw.as_text().map(|t| t.text.as_str()))
8489 .collect::<Vec<_>>()
8490 .join("\n");
8491 } else {
8492 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8493 }
8494 }
8495 #[cfg(not(feature = "mcp"))]
8496 {
8497 result_str = self.execute_tool_call(&tc.name, &tc.input)?;
8498 }
8499
8500 let hook_name = format!("__agent_{}_on_tool_call__", agent_def.name);
8502 if let Some(hook) = self.globals.get(&hook_name).cloned() {
8503 let hook_args = vec![
8504 VmValue::String(Arc::from(tc.name.as_str())),
8505 self.json_value_to_vm(&tc.input),
8506 VmValue::String(Arc::from(result_str.as_str())),
8507 ];
8508 let _ = self.call_value(hook, &hook_args);
8509 }
8510
8511 results.push((tc.name.clone(), result_str));
8512 }
8513
8514 let result_msgs = format_tool_result_messages(provider, &tool_calls, &results);
8516 messages.extend(result_msgs);
8517 }
8518 }
8519 }
8520
8521 Err(runtime_err(format!(
8522 "Agent '{}' exceeded max_turns ({})",
8523 agent_def.name, agent_def.max_turns
8524 )))
8525 }
8526
8527 #[cfg(feature = "native")]
8528 fn execute_tool_call(
8529 &mut self,
8530 tool_name: &str,
8531 input: &serde_json::Value,
8532 ) -> Result<String, TlError> {
8533 let func = self
8535 .globals
8536 .get(tool_name)
8537 .ok_or_else(|| runtime_err(format!("Agent tool function '{tool_name}' not found")))?
8538 .clone();
8539
8540 let args = self.json_to_vm_args(input);
8542
8543 let result = self.call_value(func, &args)?;
8545
8546 Ok(format!("{result}"))
8548 }
8549
8550 #[cfg(feature = "native")]
8551 fn json_to_vm_args(&self, input: &serde_json::Value) -> Vec<VmValue> {
8552 match input {
8553 serde_json::Value::Object(map) => {
8554 map.values().map(|v| self.json_value_to_vm(v)).collect()
8556 }
8557 serde_json::Value::Array(arr) => arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8558 _ => vec![self.json_value_to_vm(input)],
8559 }
8560 }
8561
8562 #[cfg(feature = "native")]
8563 fn json_value_to_vm(&self, val: &serde_json::Value) -> VmValue {
8564 match val {
8565 serde_json::Value::String(s) => VmValue::String(Arc::from(s.as_str())),
8566 serde_json::Value::Number(n) => {
8567 if let Some(i) = n.as_i64() {
8568 VmValue::Int(i)
8569 } else if let Some(f) = n.as_f64() {
8570 VmValue::Float(f)
8571 } else {
8572 VmValue::None
8573 }
8574 }
8575 serde_json::Value::Bool(b) => VmValue::Bool(*b),
8576 serde_json::Value::Null => VmValue::None,
8577 serde_json::Value::Array(arr) => VmValue::List(Box::new(
8578 arr.iter().map(|v| self.json_value_to_vm(v)).collect(),
8579 )),
8580 serde_json::Value::Object(map) => {
8581 let pairs: Vec<(Arc<str>, VmValue)> = map
8582 .iter()
8583 .map(|(k, v)| (Arc::from(k.as_str()), self.json_value_to_vm(v)))
8584 .collect();
8585 VmValue::Map(Box::new(pairs))
8586 }
8587 }
8588 }
8589
8590 #[cfg(feature = "native")]
8591 fn call_value(&mut self, func: VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
8592 match &func {
8593 VmValue::Function(_) => {
8594 let save_len = self.stack.len();
8596 let func_slot = save_len;
8597 let _args_start = func_slot + 1;
8598 self.stack.push(func.clone());
8599 for arg in args {
8600 self.stack.push(arg.clone());
8601 }
8602 self.ensure_stack(self.stack.len() + 256);
8603
8604 self.do_call(func, func_slot, 0, 1, args.len() as u8)?;
8605
8606 let entry_depth = self.frames.len() - 1;
8608 while self.frames.len() > entry_depth {
8609 if self.run_step(entry_depth)?.is_some() {
8610 break;
8611 }
8612 }
8613
8614 let result = self.stack[func_slot].clone();
8616 self.stack.truncate(save_len);
8617 Ok(result)
8618 }
8619 VmValue::Builtin(id) => {
8620 let id_u16 = *id as u16;
8621 let save_len = self.stack.len();
8622 for arg in args {
8623 self.stack.push(arg.clone());
8624 }
8625 let result = self.call_builtin(id_u16, save_len, args.len())?;
8626 self.stack.truncate(save_len);
8627 Ok(result)
8628 }
8629 _ => Err(runtime_err(format!(
8630 "Agent tool '{}' is not callable",
8631 func.type_name()
8632 ))),
8633 }
8634 }
8635
8636 #[cfg(feature = "native")]
8637 fn parse_window_type(s: &str) -> Option<tl_stream::window::WindowType> {
8638 if let Some(dur) = s.strip_prefix("tumbling:") {
8639 let ms = tl_stream::parse_duration(dur).ok()?;
8640 Some(tl_stream::window::WindowType::Tumbling { duration_ms: ms })
8641 } else if let Some(rest) = s.strip_prefix("sliding:") {
8642 let parts: Vec<&str> = rest.splitn(2, ':').collect();
8643 if parts.len() == 2 {
8644 let wms = tl_stream::parse_duration(parts[0]).ok()?;
8645 let sms = tl_stream::parse_duration(parts[1]).ok()?;
8646 Some(tl_stream::window::WindowType::Sliding {
8647 window_ms: wms,
8648 slide_ms: sms,
8649 })
8650 } else {
8651 None
8652 }
8653 } else if let Some(dur) = s.strip_prefix("session:") {
8654 let ms = tl_stream::parse_duration(dur).ok()?;
8655 Some(tl_stream::window::WindowType::Session { gap_ms: ms })
8656 } else {
8657 None
8658 }
8659 }
8660
8661 #[cfg(feature = "native")]
8662 fn handle_connector_decl(
8663 &mut self,
8664 frame_idx: usize,
8665 type_const: u8,
8666 config_const: u8,
8667 ) -> Result<VmValue, TlError> {
8668 let frame = &self.frames[frame_idx];
8669 let connector_type = match &frame.prototype.constants[type_const as usize] {
8670 Constant::String(s) => s.to_string(),
8671 _ => return Err(runtime_err("Expected string constant for connector type")),
8672 };
8673
8674 let config_args = match &frame.prototype.constants[config_const as usize] {
8675 Constant::AstExprList(args) => args.clone(),
8676 _ => return Err(runtime_err("Expected AST expr list for connector config")),
8677 };
8678
8679 let mut properties = std::collections::HashMap::new();
8680 for arg in &config_args {
8681 if let AstExpr::NamedArg { name: key, value } = arg {
8682 let val_str = match value.as_ref() {
8683 AstExpr::String(s) => s.clone(),
8684 AstExpr::Int(n) => n.to_string(),
8685 AstExpr::Float(f) => f.to_string(),
8686 AstExpr::Bool(b) => b.to_string(),
8687 other => {
8688 if let AstExpr::Ident(ident) = other {
8690 if let Some(val) = self.globals.get(ident.as_str()) {
8691 format!("{val}")
8692 } else {
8693 ident.clone()
8694 }
8695 } else {
8696 format!("{other:?}")
8697 }
8698 }
8699 };
8700 properties.insert(key.clone(), val_str);
8701 }
8702 }
8703
8704 let config = tl_stream::ConnectorConfig {
8705 name: String::new(), connector_type,
8707 properties,
8708 };
8709
8710 Ok(VmValue::Connector(Arc::new(config)))
8711 }
8712
8713 fn generator_next(&mut self, gen_arc: &Arc<Mutex<VmGenerator>>) -> Result<VmValue, TlError> {
8715 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8716 if gn.done {
8717 return Ok(VmValue::None);
8718 }
8719 match &mut gn.kind {
8720 GeneratorKind::UserDefined {
8721 prototype,
8722 upvalues,
8723 saved_stack,
8724 ip,
8725 } => {
8726 let proto = prototype.clone();
8727 let uvs = upvalues.clone();
8728 let stack_snapshot = saved_stack.clone();
8729 let saved_ip = *ip;
8730 drop(gn); let new_base = self.stack.len();
8734 let num_regs = proto.num_registers as usize;
8735 self.ensure_stack(new_base + num_regs + 1);
8736 for (i, val) in stack_snapshot.iter().enumerate() {
8738 self.stack[new_base + i] = val.clone();
8739 }
8740
8741 self.frames.push(CallFrame {
8742 prototype: proto,
8743 ip: saved_ip,
8744 base: new_base,
8745 upvalues: uvs,
8746 });
8747
8748 self.yielded_value = None;
8749 let _result = self.run()?;
8750
8751 if let Some(yielded) = self.yielded_value.take() {
8752 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8754 if let GeneratorKind::UserDefined {
8755 saved_stack, ip, ..
8756 } = &mut gn.kind
8757 {
8758 let num_regs_save = saved_stack.len();
8760 for (i, slot) in saved_stack.iter_mut().enumerate().take(num_regs_save) {
8761 if new_base + i < self.stack.len() {
8762 *slot = self.stack[new_base + i].clone();
8763 }
8764 }
8765 *ip = self.yielded_ip;
8767 }
8768 self.stack.truncate(new_base);
8769 Ok(yielded)
8770 } else {
8771 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8773 gn.done = true;
8774 self.stack.truncate(new_base);
8775 Ok(VmValue::None)
8776 }
8777 }
8778 GeneratorKind::ListIter { items, index } => {
8779 if *index < items.len() {
8780 let val = items[*index].clone();
8781 *index += 1;
8782 Ok(val)
8783 } else {
8784 gn.done = true;
8785 Ok(VmValue::None)
8786 }
8787 }
8788 GeneratorKind::Take { source, remaining } => {
8789 if *remaining == 0 {
8790 gn.done = true;
8791 return Ok(VmValue::None);
8792 }
8793 *remaining -= 1;
8794 let src = source.clone();
8795 drop(gn);
8796 let val = self.generator_next(&src)?;
8797 if matches!(val, VmValue::None) {
8798 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8799 gn.done = true;
8800 }
8801 Ok(val)
8802 }
8803 GeneratorKind::Skip { source, remaining } => {
8804 let src = source.clone();
8805 let skip_n = *remaining;
8806 *remaining = 0;
8807 drop(gn);
8808 for _ in 0..skip_n {
8810 let val = self.generator_next(&src)?;
8811 if matches!(val, VmValue::None) {
8812 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8813 gn.done = true;
8814 return Ok(VmValue::None);
8815 }
8816 }
8817 let val = self.generator_next(&src)?;
8818 if matches!(val, VmValue::None) {
8819 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8820 gn.done = true;
8821 }
8822 Ok(val)
8823 }
8824 GeneratorKind::Map { source, func } => {
8825 let src = source.clone();
8826 let f = func.clone();
8827 drop(gn);
8828 let val = self.generator_next(&src)?;
8829 if matches!(val, VmValue::None) {
8830 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8831 gn.done = true;
8832 return Ok(VmValue::None);
8833 }
8834 self.call_vm_function(&f, &[val])
8835 }
8836 GeneratorKind::Filter { source, func } => {
8837 let src = source.clone();
8838 let f = func.clone();
8839 drop(gn);
8840 loop {
8841 let val = self.generator_next(&src)?;
8842 if matches!(val, VmValue::None) {
8843 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8844 gn.done = true;
8845 return Ok(VmValue::None);
8846 }
8847 let test = self.call_vm_function(&f, std::slice::from_ref(&val))?;
8848 if test.is_truthy() {
8849 return Ok(val);
8850 }
8851 }
8852 }
8853 GeneratorKind::Chain {
8854 first,
8855 second,
8856 on_second,
8857 } => {
8858 if !*on_second {
8859 let src = first.clone();
8860 drop(gn);
8861 let val = self.generator_next(&src)?;
8862 if matches!(val, VmValue::None) {
8863 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8864 if let GeneratorKind::Chain {
8865 on_second, second, ..
8866 } = &mut gn.kind
8867 {
8868 *on_second = true;
8869 let src2 = second.clone();
8870 drop(gn);
8871 return self.generator_next(&src2);
8872 }
8873 }
8874 Ok(val)
8875 } else {
8876 let src = second.clone();
8877 drop(gn);
8878 let val = self.generator_next(&src)?;
8879 if matches!(val, VmValue::None) {
8880 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8881 gn.done = true;
8882 }
8883 Ok(val)
8884 }
8885 }
8886 GeneratorKind::Zip { first, second } => {
8887 let src1 = first.clone();
8888 let src2 = second.clone();
8889 drop(gn);
8890 let val1 = self.generator_next(&src1)?;
8891 let val2 = self.generator_next(&src2)?;
8892 if matches!(val1, VmValue::None) || matches!(val2, VmValue::None) {
8893 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8894 gn.done = true;
8895 return Ok(VmValue::None);
8896 }
8897 Ok(VmValue::List(Box::new(vec![val1, val2])))
8898 }
8899 GeneratorKind::Enumerate { source, index } => {
8900 let src = source.clone();
8901 let idx = *index;
8902 *index += 1;
8903 drop(gn);
8904 let val = self.generator_next(&src)?;
8905 if matches!(val, VmValue::None) {
8906 let mut gn = gen_arc.lock().unwrap_or_else(|e| e.into_inner());
8907 gn.done = true;
8908 return Ok(VmValue::None);
8909 }
8910 Ok(VmValue::List(Box::new(vec![VmValue::Int(idx as i64), val])))
8911 }
8912 }
8913 }
8914
8915 #[cfg(feature = "native")]
8917 fn process_schema_global(&mut self, s: &str) {
8918 let rest = &s["__schema__:".len()..];
8920 let parts: Vec<&str> = rest.splitn(3, ':').collect();
8921 if parts.len() < 2 {
8922 return;
8923 }
8924
8925 let schema_name = parts[0];
8926 let mut version: i64 = 0;
8927 let fields_str;
8928
8929 if parts.len() == 3 && parts[1].starts_with('v') {
8930 version = parts[1][1..].parse().unwrap_or(0);
8932 fields_str = parts[2];
8933 } else if parts.len() == 3 {
8934 fields_str = &rest[schema_name.len() + 1..];
8936 } else {
8937 fields_str = parts[1];
8938 }
8939
8940 if version == 0 {
8941 return;
8942 } let mut arrow_fields = Vec::new();
8945 for field_pair in fields_str.split(',') {
8946 let kv: Vec<&str> = field_pair.splitn(2, ':').collect();
8947 if kv.len() == 2 {
8948 let fname = kv[0].trim();
8949 let ftype = kv[1].trim();
8950 let type_name = if ftype.starts_with("Simple(\"") && ftype.ends_with("\")") {
8952 &ftype[8..ftype.len() - 2]
8953 } else {
8954 ftype
8955 };
8956 let dt = crate::schema::type_name_to_arrow_pub(type_name);
8957 arrow_fields.push(tl_data::ArrowField::new(fname, dt, true));
8958 }
8959 }
8960
8961 if !arrow_fields.is_empty() {
8962 let schema = std::sync::Arc::new(tl_data::ArrowSchema::new(arrow_fields));
8963 let _ = self.schema_registry.register(
8964 schema_name,
8965 version,
8966 schema,
8967 crate::schema::SchemaMetadata::default(),
8968 );
8969 }
8970 }
8971
8972 #[cfg(feature = "native")]
8974 fn process_migrate_global(&mut self, s: &str) {
8975 let rest = &s["__migrate__:".len()..];
8977 let parts: Vec<&str> = rest.splitn(4, ':').collect();
8978 if parts.len() < 4 {
8979 return;
8980 }
8981
8982 let schema_name = parts[0];
8983 let from_ver: i64 = parts[1].parse().unwrap_or(0);
8984 let to_ver: i64 = parts[2].parse().unwrap_or(0);
8985 let ops_str = parts[3];
8986
8987 let mut ops = Vec::new();
8988 for op_str in ops_str.split(';') {
8989 let op_parts: Vec<&str> = op_str.splitn(4, ':').collect();
8990 if op_parts.is_empty() {
8991 continue;
8992 }
8993 match op_parts[0] {
8994 "add" if op_parts.len() >= 3 => {
8995 let name = op_parts[1].to_string();
8996 let type_raw = op_parts[2];
8998 let type_name =
8999 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
9000 type_raw[8..type_raw.len() - 2].to_string()
9001 } else {
9002 type_raw.to_string()
9003 };
9004 let default = if op_parts.len() >= 4 && op_parts[3].starts_with("default:") {
9005 Some(
9006 op_parts[3]["default:".len()..]
9007 .trim_matches('"')
9008 .to_string(),
9009 )
9010 } else {
9011 None
9012 };
9013 ops.push(crate::schema::MigrationOp::AddColumn {
9014 name,
9015 type_name,
9016 default,
9017 });
9018 }
9019 "drop" if op_parts.len() >= 2 => {
9020 ops.push(crate::schema::MigrationOp::DropColumn {
9021 name: op_parts[1].to_string(),
9022 });
9023 }
9024 "rename" if op_parts.len() >= 3 => {
9025 ops.push(crate::schema::MigrationOp::RenameColumn {
9026 from: op_parts[1].to_string(),
9027 to: op_parts[2].to_string(),
9028 });
9029 }
9030 "alter" if op_parts.len() >= 3 => {
9031 let type_raw = op_parts[2];
9032 let type_name =
9033 if type_raw.starts_with("Simple(\"") && type_raw.ends_with("\")") {
9034 type_raw[8..type_raw.len() - 2].to_string()
9035 } else {
9036 type_raw.to_string()
9037 };
9038 ops.push(crate::schema::MigrationOp::AlterType {
9039 column: op_parts[1].to_string(),
9040 new_type: type_name,
9041 });
9042 }
9043 _ => {}
9044 }
9045 }
9046
9047 let _ = self
9048 .schema_registry
9049 .apply_migration(schema_name, from_ver, to_ver, &ops);
9050 }
9051
9052 fn deep_clone_value(&self, val: &VmValue) -> Result<VmValue, TlError> {
9055 match val {
9056 VmValue::List(items) => {
9057 let cloned: Result<Vec<_>, _> =
9058 items.iter().map(|v| self.deep_clone_value(v)).collect();
9059 Ok(VmValue::List(Box::new(cloned?)))
9060 }
9061 VmValue::Map(pairs) => {
9062 let cloned: Result<Vec<_>, _> = pairs
9063 .iter()
9064 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9065 .collect();
9066 Ok(VmValue::Map(Box::new(cloned?)))
9067 }
9068 VmValue::Set(items) => {
9069 let cloned: Result<Vec<_>, _> =
9070 items.iter().map(|v| self.deep_clone_value(v)).collect();
9071 Ok(VmValue::Set(Box::new(cloned?)))
9072 }
9073 VmValue::StructInstance(inst) => {
9074 let cloned_fields: Result<Vec<_>, _> = inst
9075 .fields
9076 .iter()
9077 .map(|(k, v)| Ok((k.clone(), self.deep_clone_value(v)?)))
9078 .collect();
9079 Ok(VmValue::StructInstance(Arc::new(VmStructInstance {
9080 type_name: inst.type_name.clone(),
9081 fields: cloned_fields?,
9082 })))
9083 }
9084 VmValue::EnumInstance(e) => {
9085 let cloned_fields: Result<Vec<_>, _> =
9086 e.fields.iter().map(|v| self.deep_clone_value(v)).collect();
9087 Ok(VmValue::EnumInstance(Arc::new(VmEnumInstance {
9088 type_name: e.type_name.clone(),
9089 variant: e.variant.clone(),
9090 fields: cloned_fields?,
9091 })))
9092 }
9093 #[cfg(feature = "gpu")]
9094 VmValue::GpuTensor(gt) => {
9095 let cloned = tl_gpu::GpuTensor::clone(gt.as_ref());
9096 Ok(VmValue::GpuTensor(Arc::new(cloned)))
9097 }
9098 VmValue::Ref(inner) => self.deep_clone_value(inner),
9099 VmValue::Moved => Err(runtime_err("Cannot clone a moved value".to_string())),
9100 VmValue::Task(_) => Err(runtime_err("Cannot clone a task".to_string())),
9101 VmValue::Channel(_) => Err(runtime_err("Cannot clone a channel".to_string())),
9102 VmValue::Generator(_) => Err(runtime_err("Cannot clone a generator".to_string())),
9103 other => Ok(other.clone()),
9104 }
9105 }
9106
9107 pub fn dispatch_method(
9108 &mut self,
9109 obj: VmValue,
9110 method: &str,
9111 args: &[VmValue],
9112 ) -> Result<VmValue, TlError> {
9113 if method == "clone" {
9115 return self.deep_clone_value(&obj);
9116 }
9117 let obj = match obj {
9119 VmValue::Ref(inner) => inner.as_ref().clone(),
9120 other => other,
9121 };
9122 match &obj {
9123 VmValue::String(s) => self.dispatch_string_method(s.clone(), method, args),
9124 VmValue::List(items) => self.dispatch_list_method((**items).clone(), method, args),
9125 VmValue::Map(pairs) => self.dispatch_map_method((**pairs).clone(), method, args),
9126 VmValue::Set(items) => self.dispatch_set_method((**items).clone(), method, args),
9127 VmValue::Module(m) => {
9128 if let Some(func) = m.exports.get(method).cloned() {
9129 self.call_vm_function(&func, args)
9130 } else {
9131 Err(runtime_err(format!(
9132 "Module '{}' has no export '{}'",
9133 m.name, method
9134 )))
9135 }
9136 }
9137 VmValue::StructInstance(inst) => {
9138 let mangled = format!("{}::{}", inst.type_name, method);
9140 if let Some(func) = self.globals.get(&mangled).cloned() {
9141 let mut all_args = vec![obj.clone()];
9142 all_args.extend_from_slice(args);
9143 self.call_vm_function(&func, &all_args)
9144 } else {
9145 Err(runtime_err(format!(
9146 "No method '{}' on struct '{}'",
9147 method, inst.type_name
9148 )))
9149 }
9150 }
9151 #[cfg(feature = "python")]
9152 VmValue::PyObject(wrapper) => crate::python::py_call_method(wrapper, method, args),
9153 #[cfg(feature = "gpu")]
9154 VmValue::GpuTensor(gt) => match method {
9155 "to_cpu" => {
9156 let cpu = gt.to_cpu().map_err(runtime_err)?;
9157 Ok(VmValue::Tensor(Arc::new(cpu)))
9158 }
9159 "shape" => {
9160 let shape_list = Box::new(
9161 gt.shape
9162 .iter()
9163 .map(|&d| VmValue::Int(d as i64))
9164 .collect::<Vec<_>>(),
9165 );
9166 Ok(VmValue::List(shape_list))
9167 }
9168 "dtype" => Ok(VmValue::String(Arc::from(format!("{}", gt.dtype).as_str()))),
9169 _ => Err(runtime_err(format!("No method '{}' on gpu_tensor", method))),
9170 },
9171 _ => {
9172 let type_name = obj.type_name();
9174 let mangled = format!("{}::{}", type_name, method);
9175 if let Some(func) = self.globals.get(&mangled).cloned() {
9176 let mut all_args = vec![obj];
9177 all_args.extend_from_slice(args);
9178 self.call_vm_function(&func, &all_args)
9179 } else {
9180 Err(runtime_err(format!(
9181 "No method '{}' on type '{}'",
9182 method, type_name
9183 )))
9184 }
9185 }
9186 }
9187 }
9188
9189 fn dispatch_string_method(
9191 &self,
9192 s: Arc<str>,
9193 method: &str,
9194 args: &[VmValue],
9195 ) -> Result<VmValue, TlError> {
9196 match method {
9197 "len" => Ok(VmValue::Int(s.len() as i64)),
9198 "split" => {
9199 let sep = match args.first() {
9200 Some(VmValue::String(sep)) => sep.to_string(),
9201 _ => return Err(runtime_err("split() expects a string separator")),
9202 };
9203 let parts: Vec<VmValue> = s
9204 .split(&sep)
9205 .map(|p| VmValue::String(Arc::from(p)))
9206 .collect();
9207 Ok(VmValue::List(Box::new(parts)))
9208 }
9209 "trim" => Ok(VmValue::String(Arc::from(s.trim()))),
9210 "contains" => {
9211 let needle = match args.first() {
9212 Some(VmValue::String(n)) => n.to_string(),
9213 _ => return Err(runtime_err("contains() expects a string")),
9214 };
9215 Ok(VmValue::Bool(s.contains(&needle)))
9216 }
9217 "replace" => {
9218 if args.len() < 2 {
9219 return Err(runtime_err("replace() expects 2 arguments (old, new)"));
9220 }
9221 let old = match &args[0] {
9222 VmValue::String(s) => s.to_string(),
9223 _ => return Err(runtime_err("replace() arg must be string")),
9224 };
9225 let new = match &args[1] {
9226 VmValue::String(s) => s.to_string(),
9227 _ => return Err(runtime_err("replace() arg must be string")),
9228 };
9229 Ok(VmValue::String(Arc::from(s.replace(&old, &new).as_str())))
9230 }
9231 "starts_with" => {
9232 let prefix = match args.first() {
9233 Some(VmValue::String(p)) => p.to_string(),
9234 _ => return Err(runtime_err("starts_with() expects a string")),
9235 };
9236 Ok(VmValue::Bool(s.starts_with(&prefix)))
9237 }
9238 "ends_with" => {
9239 let suffix = match args.first() {
9240 Some(VmValue::String(p)) => p.to_string(),
9241 _ => return Err(runtime_err("ends_with() expects a string")),
9242 };
9243 Ok(VmValue::Bool(s.ends_with(&suffix)))
9244 }
9245 "to_upper" => Ok(VmValue::String(Arc::from(s.to_uppercase().as_str()))),
9246 "to_lower" => Ok(VmValue::String(Arc::from(s.to_lowercase().as_str()))),
9247 "chars" => {
9248 let chars: Vec<VmValue> = s
9249 .chars()
9250 .map(|c| VmValue::String(Arc::from(c.to_string().as_str())))
9251 .collect();
9252 Ok(VmValue::List(Box::new(chars)))
9253 }
9254 "repeat" => {
9255 let n = match args.first() {
9256 Some(VmValue::Int(n)) => *n as usize,
9257 _ => return Err(runtime_err("repeat() expects an integer")),
9258 };
9259 Ok(VmValue::String(Arc::from(s.repeat(n).as_str())))
9260 }
9261 "index_of" => {
9262 let needle = match args.first() {
9263 Some(VmValue::String(n)) => n.to_string(),
9264 _ => return Err(runtime_err("index_of() expects a string")),
9265 };
9266 Ok(VmValue::Int(
9267 s.find(&needle).map(|i| i as i64).unwrap_or(-1),
9268 ))
9269 }
9270 "substring" => {
9271 if args.len() < 2 {
9272 return Err(runtime_err("substring() expects start and end"));
9273 }
9274 let start = match &args[0] {
9275 VmValue::Int(n) => *n as usize,
9276 _ => return Err(runtime_err("substring() expects integers")),
9277 };
9278 let end = match &args[1] {
9279 VmValue::Int(n) => *n as usize,
9280 _ => return Err(runtime_err("substring() expects integers")),
9281 };
9282 let end = end.min(s.len());
9283 let start = start.min(end);
9284 Ok(VmValue::String(Arc::from(&s[start..end])))
9285 }
9286 "pad_left" => {
9287 if args.is_empty() {
9288 return Err(runtime_err("pad_left() expects width"));
9289 }
9290 let width = match &args[0] {
9291 VmValue::Int(n) => *n as usize,
9292 _ => return Err(runtime_err("pad_left() expects integer width")),
9293 };
9294 let ch = match args.get(1) {
9295 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9296 _ => ' ',
9297 };
9298 if s.len() >= width {
9299 Ok(VmValue::String(s))
9300 } else {
9301 Ok(VmValue::String(Arc::from(
9302 format!(
9303 "{}{}",
9304 std::iter::repeat_n(ch, width - s.len()).collect::<String>(),
9305 s
9306 )
9307 .as_str(),
9308 )))
9309 }
9310 }
9311 "pad_right" => {
9312 if args.is_empty() {
9313 return Err(runtime_err("pad_right() expects width"));
9314 }
9315 let width = match &args[0] {
9316 VmValue::Int(n) => *n as usize,
9317 _ => return Err(runtime_err("pad_right() expects integer width")),
9318 };
9319 let ch = match args.get(1) {
9320 Some(VmValue::String(c)) => c.chars().next().unwrap_or(' '),
9321 _ => ' ',
9322 };
9323 if s.len() >= width {
9324 Ok(VmValue::String(s))
9325 } else {
9326 Ok(VmValue::String(Arc::from(
9327 format!(
9328 "{}{}",
9329 s,
9330 std::iter::repeat_n(ch, width - s.len()).collect::<String>()
9331 )
9332 .as_str(),
9333 )))
9334 }
9335 }
9336 "join" => {
9337 let items = match args.first() {
9339 Some(VmValue::List(items)) => items,
9340 _ => return Err(runtime_err("join() expects a list")),
9341 };
9342 let parts: Vec<String> = items.iter().map(|v| format!("{v}")).collect();
9343 Ok(VmValue::String(Arc::from(parts.join(s.as_ref()).as_str())))
9344 }
9345 "trim_start" => Ok(VmValue::String(Arc::from(s.trim_start()))),
9346 "trim_end" => Ok(VmValue::String(Arc::from(s.trim_end()))),
9347 "count" => {
9348 if args.is_empty() {
9349 return Err(runtime_err("count() expects a substring"));
9350 }
9351 if let VmValue::String(sub) = &args[0] {
9352 Ok(VmValue::Int(s.matches(sub.as_ref()).count() as i64))
9353 } else {
9354 Err(runtime_err("count() expects a string"))
9355 }
9356 }
9357 "is_empty" => Ok(VmValue::Bool(s.is_empty())),
9358 "is_numeric" => Ok(VmValue::Bool(
9359 s.chars()
9360 .all(|c| c.is_ascii_digit() || c == '.' || c == '-'),
9361 )),
9362 "is_alpha" => Ok(VmValue::Bool(
9363 !s.is_empty() && s.chars().all(|c| c.is_alphabetic()),
9364 )),
9365 "strip_prefix" => {
9366 if args.is_empty() {
9367 return Err(runtime_err("strip_prefix() expects a string"));
9368 }
9369 if let VmValue::String(prefix) = &args[0] {
9370 match s.strip_prefix(prefix.as_ref()) {
9371 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9372 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9373 }
9374 } else {
9375 Err(runtime_err("strip_prefix() expects a string"))
9376 }
9377 }
9378 "strip_suffix" => {
9379 if args.is_empty() {
9380 return Err(runtime_err("strip_suffix() expects a string"));
9381 }
9382 if let VmValue::String(suffix) = &args[0] {
9383 match s.strip_suffix(suffix.as_ref()) {
9384 Some(rest) => Ok(VmValue::String(Arc::from(rest))),
9385 None => Ok(VmValue::String(Arc::from(s.as_ref()))),
9386 }
9387 } else {
9388 Err(runtime_err("strip_suffix() expects a string"))
9389 }
9390 }
9391 _ => Err(runtime_err(format!("No method '{}' on string", method))),
9392 }
9393 }
9394
9395 fn dispatch_list_method(
9397 &mut self,
9398 items: Vec<VmValue>,
9399 method: &str,
9400 args: &[VmValue],
9401 ) -> Result<VmValue, TlError> {
9402 match method {
9403 "len" => Ok(VmValue::Int(items.len() as i64)),
9404 "push" => {
9405 if args.is_empty() {
9406 return Err(runtime_err("push() expects 1 argument"));
9407 }
9408 let mut new_items = items;
9409 new_items.push(args[0].clone());
9410 Ok(VmValue::List(Box::new(new_items)))
9411 }
9412 "map" => {
9413 if args.is_empty() {
9414 return Err(runtime_err("map() expects a function"));
9415 }
9416 let func = &args[0];
9417 let mut result = Vec::new();
9418 for item in items {
9419 let val = self.call_vm_function(func, &[item])?;
9420 result.push(val);
9421 }
9422 Ok(VmValue::List(Box::new(result)))
9423 }
9424 "filter" => {
9425 if args.is_empty() {
9426 return Err(runtime_err("filter() expects a function"));
9427 }
9428 let func = &args[0];
9429 let mut result = Vec::new();
9430 for item in items {
9431 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9432 if val.is_truthy() {
9433 result.push(item);
9434 }
9435 }
9436 Ok(VmValue::List(Box::new(result)))
9437 }
9438 "reduce" => {
9439 if args.len() < 2 {
9440 return Err(runtime_err("reduce() expects initial value and function"));
9441 }
9442 let mut acc = args[0].clone();
9443 let func = &args[1];
9444 for item in items {
9445 acc = self.call_vm_function(func, &[acc, item])?;
9446 }
9447 Ok(acc)
9448 }
9449 "sort" => {
9450 let mut sorted = items;
9451 sorted.sort_by(|a, b| match (a, b) {
9452 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9453 (VmValue::Float(x), VmValue::Float(y)) => {
9454 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9455 }
9456 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9457 _ => std::cmp::Ordering::Equal,
9458 });
9459 Ok(VmValue::List(Box::new(sorted)))
9460 }
9461 "reverse" => {
9462 let mut reversed = items;
9463 reversed.reverse();
9464 Ok(VmValue::List(Box::new(reversed)))
9465 }
9466 "contains" => {
9467 if args.is_empty() {
9468 return Err(runtime_err("contains() expects a value"));
9469 }
9470 let needle = &args[0];
9471 let found = items.iter().any(|item| match (item, needle) {
9472 (VmValue::Int(a), VmValue::Int(b)) => a == b,
9473 (VmValue::Float(a), VmValue::Float(b)) => a == b,
9474 (VmValue::String(a), VmValue::String(b)) => a == b,
9475 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9476 (VmValue::None, VmValue::None) => true,
9477 _ => false,
9478 });
9479 Ok(VmValue::Bool(found))
9480 }
9481 "index_of" => {
9482 if args.is_empty() {
9483 return Err(runtime_err("index_of() expects a value"));
9484 }
9485 let needle = &args[0];
9486 let idx = items.iter().position(|item| match (item, needle) {
9487 (VmValue::Int(a), VmValue::Int(b)) => a == b,
9488 (VmValue::Float(a), VmValue::Float(b)) => a == b,
9489 (VmValue::String(a), VmValue::String(b)) => a == b,
9490 (VmValue::Bool(a), VmValue::Bool(b)) => a == b,
9491 (VmValue::None, VmValue::None) => true,
9492 _ => false,
9493 });
9494 Ok(VmValue::Int(idx.map(|i| i as i64).unwrap_or(-1)))
9495 }
9496 "slice" => {
9497 if args.len() < 2 {
9498 return Err(runtime_err("slice() expects start and end"));
9499 }
9500 let start = match &args[0] {
9501 VmValue::Int(n) => *n as usize,
9502 _ => return Err(runtime_err("slice() expects integers")),
9503 };
9504 let end = match &args[1] {
9505 VmValue::Int(n) => *n as usize,
9506 _ => return Err(runtime_err("slice() expects integers")),
9507 };
9508 let end = end.min(items.len());
9509 let start = start.min(end);
9510 Ok(VmValue::List(Box::new(items[start..end].to_vec())))
9511 }
9512 "flat_map" => {
9513 if args.is_empty() {
9514 return Err(runtime_err("flat_map() expects a function"));
9515 }
9516 let func = &args[0];
9517 let mut result = Vec::new();
9518 for item in items {
9519 let val = self.call_vm_function(func, &[item])?;
9520 match val {
9521 VmValue::List(sub) => result.extend(*sub),
9522 other => result.push(other),
9523 }
9524 }
9525 Ok(VmValue::List(Box::new(result)))
9526 }
9527 "find" => {
9528 if args.is_empty() {
9529 return Err(runtime_err("find() expects a predicate function"));
9530 }
9531 let func = &args[0];
9532 for item in items {
9533 let val = self.call_vm_function(func, std::slice::from_ref(&item))?;
9534 if val.is_truthy() {
9535 return Ok(item);
9536 }
9537 }
9538 Ok(VmValue::None)
9539 }
9540 "sort_by" => {
9541 if args.is_empty() {
9542 return Err(runtime_err("sort_by() expects a key function"));
9543 }
9544 let func = &args[0];
9545 let mut keyed: Vec<(VmValue, VmValue)> = Vec::with_capacity(items.len());
9546 for item in items {
9547 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9548 keyed.push((key, item));
9549 }
9550 keyed.sort_by(|(a, _), (b, _)| match (a, b) {
9551 (VmValue::Int(x), VmValue::Int(y)) => x.cmp(y),
9552 (VmValue::Float(x), VmValue::Float(y)) => {
9553 x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
9554 }
9555 (VmValue::String(x), VmValue::String(y)) => x.cmp(y),
9556 _ => std::cmp::Ordering::Equal,
9557 });
9558 Ok(VmValue::List(Box::new(
9559 keyed.into_iter().map(|(_, v)| v).collect(),
9560 )))
9561 }
9562 "group_by" => {
9563 if args.is_empty() {
9564 return Err(runtime_err("group_by() expects a key function"));
9565 }
9566 let func = &args[0];
9567 let mut groups: Vec<(Arc<str>, Vec<VmValue>)> = Vec::new();
9568 for item in items {
9569 let key = self.call_vm_function(func, std::slice::from_ref(&item))?;
9570 let key_str: Arc<str> = match &key {
9571 VmValue::String(s) => s.clone(),
9572 other => Arc::from(format!("{other}").as_str()),
9573 };
9574 if let Some(group) = groups.iter_mut().find(|(k, _)| *k == key_str) {
9575 group.1.push(item);
9576 } else {
9577 groups.push((key_str, vec![item]));
9578 }
9579 }
9580 let map_pairs: Vec<(Arc<str>, VmValue)> = groups
9581 .into_iter()
9582 .map(|(k, v)| (k, VmValue::List(Box::new(v))))
9583 .collect();
9584 Ok(VmValue::Map(Box::new(map_pairs)))
9585 }
9586 "unique" => {
9587 let mut seen = Vec::new();
9588 let mut result = Vec::new();
9589 for item in &items {
9590 let is_dup = seen.iter().any(|s| vm_values_equal(s, item));
9591 if !is_dup {
9592 seen.push(item.clone());
9593 result.push(item.clone());
9594 }
9595 }
9596 Ok(VmValue::List(Box::new(result)))
9597 }
9598 "flatten" => {
9599 let mut result = Vec::new();
9600 for item in items {
9601 match item {
9602 VmValue::List(sub) => result.extend(*sub),
9603 other => result.push(other),
9604 }
9605 }
9606 Ok(VmValue::List(Box::new(result)))
9607 }
9608 "chunk" => {
9609 if args.is_empty() {
9610 return Err(runtime_err("chunk() expects a size"));
9611 }
9612 let n = match &args[0] {
9613 VmValue::Int(n) if *n > 0 => *n as usize,
9614 _ => return Err(runtime_err("chunk() expects a positive integer")),
9615 };
9616 let chunks: Vec<VmValue> = items
9617 .chunks(n)
9618 .map(|c| VmValue::List(Box::new(c.to_vec())))
9619 .collect();
9620 Ok(VmValue::List(Box::new(chunks)))
9621 }
9622 "insert" => {
9623 if args.len() < 2 {
9624 return Err(runtime_err("insert() expects index and value"));
9625 }
9626 let idx = match &args[0] {
9627 VmValue::Int(n) => *n as usize,
9628 _ => return Err(runtime_err("insert() expects integer index")),
9629 };
9630 let mut new_items = items;
9631 if idx > new_items.len() {
9632 return Err(runtime_err("insert() index out of bounds"));
9633 }
9634 new_items.insert(idx, args[1].clone());
9635 Ok(VmValue::List(Box::new(new_items)))
9636 }
9637 "remove_at" => {
9638 if args.is_empty() {
9639 return Err(runtime_err("remove_at() expects an index"));
9640 }
9641 let idx = match &args[0] {
9642 VmValue::Int(n) => *n as usize,
9643 _ => return Err(runtime_err("remove_at() expects integer index")),
9644 };
9645 let mut new_items = items;
9646 if idx >= new_items.len() {
9647 return Err(runtime_err("remove_at() index out of bounds"));
9648 }
9649 let removed = new_items.remove(idx);
9650 Ok(removed)
9651 }
9652 "is_empty" => Ok(VmValue::Bool(items.is_empty())),
9653 "sum" => {
9654 let mut int_sum: i64 = 0;
9655 let mut has_float = false;
9656 let mut float_sum: f64 = 0.0;
9657 for item in &items {
9658 match item {
9659 VmValue::Int(n) => {
9660 if has_float {
9661 float_sum += *n as f64;
9662 } else {
9663 int_sum += n;
9664 }
9665 }
9666 VmValue::Float(f) => {
9667 if !has_float {
9668 has_float = true;
9669 float_sum = int_sum as f64;
9670 }
9671 float_sum += f;
9672 }
9673 _ => return Err(runtime_err("sum() requires numeric list")),
9674 }
9675 }
9676 if has_float {
9677 Ok(VmValue::Float(float_sum))
9678 } else {
9679 Ok(VmValue::Int(int_sum))
9680 }
9681 }
9682 "min" => {
9683 if items.is_empty() {
9684 return Ok(VmValue::None);
9685 }
9686 let mut min_val = items[0].clone();
9687 for item in &items[1..] {
9688 match (&min_val, item) {
9689 (VmValue::Int(a), VmValue::Int(b)) if b < a => min_val = item.clone(),
9690 (VmValue::Float(a), VmValue::Float(b)) if b < a => min_val = item.clone(),
9691 _ => {}
9692 }
9693 }
9694 Ok(min_val)
9695 }
9696 "max" => {
9697 if items.is_empty() {
9698 return Ok(VmValue::None);
9699 }
9700 let mut max_val = items[0].clone();
9701 for item in &items[1..] {
9702 match (&max_val, item) {
9703 (VmValue::Int(a), VmValue::Int(b)) if b > a => max_val = item.clone(),
9704 (VmValue::Float(a), VmValue::Float(b)) if b > a => max_val = item.clone(),
9705 _ => {}
9706 }
9707 }
9708 Ok(max_val)
9709 }
9710 "each" => {
9711 if args.is_empty() {
9712 return Err(runtime_err("each() expects a function"));
9713 }
9714 let func = &args[0];
9715 for item in items {
9716 self.call_vm_function(func, &[item])?;
9717 }
9718 Ok(VmValue::None)
9719 }
9720 "zip" => {
9721 if args.is_empty() {
9722 return Err(runtime_err("zip() expects a list"));
9723 }
9724 let other = match &args[0] {
9725 VmValue::List(other) => other.as_slice(),
9726 _ => return Err(runtime_err("zip() expects a list")),
9727 };
9728 let len = items.len().min(other.len());
9729 let zipped: Vec<VmValue> = items[..len]
9730 .iter()
9731 .zip(other[..len].iter())
9732 .map(|(a, b)| VmValue::List(Box::new(vec![a.clone(), b.clone()])))
9733 .collect();
9734 Ok(VmValue::List(Box::new(zipped)))
9735 }
9736 "join" => {
9737 let sep = match args.first() {
9738 Some(VmValue::String(s)) => s.as_ref(),
9739 _ => "",
9740 };
9741 let s: String = items
9742 .iter()
9743 .map(|v| format!("{v}"))
9744 .collect::<Vec<_>>()
9745 .join(sep);
9746 Ok(VmValue::String(Arc::from(s.as_str())))
9747 }
9748 _ => Err(runtime_err(format!("No method '{}' on list", method))),
9749 }
9750 }
9751
9752 fn dispatch_map_method(
9754 &mut self,
9755 pairs: Vec<(Arc<str>, VmValue)>,
9756 method: &str,
9757 args: &[VmValue],
9758 ) -> Result<VmValue, TlError> {
9759 match method {
9760 "len" => Ok(VmValue::Int(pairs.len() as i64)),
9761 "keys" => Ok(VmValue::List(Box::new(
9762 pairs
9763 .iter()
9764 .map(|(k, _)| VmValue::String(k.clone()))
9765 .collect(),
9766 ))),
9767 "values" => Ok(VmValue::List(Box::new(
9768 pairs.iter().map(|(_, v)| v.clone()).collect(),
9769 ))),
9770 "contains_key" => {
9771 if args.is_empty() {
9772 return Err(runtime_err("contains_key() expects a key"));
9773 }
9774 if let VmValue::String(key) = &args[0] {
9775 Ok(VmValue::Bool(
9776 pairs.iter().any(|(k, _)| k.as_ref() == key.as_ref()),
9777 ))
9778 } else {
9779 Err(runtime_err("contains_key() expects a string key"))
9780 }
9781 }
9782 "remove" => {
9783 if args.is_empty() {
9784 return Err(runtime_err("remove() expects a key"));
9785 }
9786 if let VmValue::String(key) = &args[0] {
9787 let new_pairs: Vec<(Arc<str>, VmValue)> = pairs
9788 .into_iter()
9789 .filter(|(k, _)| k.as_ref() != key.as_ref())
9790 .collect();
9791 Ok(VmValue::Map(Box::new(new_pairs)))
9792 } else {
9793 Err(runtime_err("remove() expects a string key"))
9794 }
9795 }
9796 "get" => {
9797 if args.is_empty() {
9798 return Err(runtime_err("get() expects a key"));
9799 }
9800 if let VmValue::String(key) = &args[0] {
9801 let default = args.get(1).cloned().unwrap_or(VmValue::None);
9802 let found = pairs.iter().find(|(k, _)| k.as_ref() == key.as_ref());
9803 Ok(found.map(|(_, v)| v.clone()).unwrap_or(default))
9804 } else {
9805 Err(runtime_err("get() expects a string key"))
9806 }
9807 }
9808 "merge" => {
9809 if args.is_empty() {
9810 return Err(runtime_err("merge() expects a map"));
9811 }
9812 if let VmValue::Map(other) = &args[0] {
9813 let mut merged = pairs;
9814 for (k, v) in other.iter() {
9815 if let Some(existing) =
9816 merged.iter_mut().find(|(mk, _)| mk.as_ref() == k.as_ref())
9817 {
9818 existing.1 = v.clone();
9819 } else {
9820 merged.push((k.clone(), v.clone()));
9821 }
9822 }
9823 Ok(VmValue::Map(Box::new(merged)))
9824 } else {
9825 Err(runtime_err("merge() expects a map"))
9826 }
9827 }
9828 "entries" => {
9829 let entries: Vec<VmValue> = pairs
9830 .iter()
9831 .map(|(k, v)| {
9832 VmValue::List(Box::new(vec![VmValue::String(k.clone()), v.clone()]))
9833 })
9834 .collect();
9835 Ok(VmValue::List(Box::new(entries)))
9836 }
9837 "map_values" => {
9838 if args.is_empty() {
9839 return Err(runtime_err("map_values() expects a function"));
9840 }
9841 let func = &args[0];
9842 let mut result = Vec::new();
9843 for (k, v) in pairs {
9844 let new_v = self.call_vm_function(func, &[v])?;
9845 result.push((k, new_v));
9846 }
9847 Ok(VmValue::Map(Box::new(result)))
9848 }
9849 "filter" => {
9850 if args.is_empty() {
9851 return Err(runtime_err("filter() expects a predicate function"));
9852 }
9853 let func = &args[0];
9854 let mut result = Vec::new();
9855 for (k, v) in pairs {
9856 let val =
9857 self.call_vm_function(func, &[VmValue::String(k.clone()), v.clone()])?;
9858 if val.is_truthy() {
9859 result.push((k, v));
9860 }
9861 }
9862 Ok(VmValue::Map(Box::new(result)))
9863 }
9864 "set" => {
9865 if args.len() < 2 {
9866 return Err(runtime_err("set() expects key and value"));
9867 }
9868 if let VmValue::String(key) = &args[0] {
9869 let mut new_pairs = pairs;
9870 if let Some(existing) = new_pairs
9871 .iter_mut()
9872 .find(|(k, _)| k.as_ref() == key.as_ref())
9873 {
9874 existing.1 = args[1].clone();
9875 } else {
9876 new_pairs.push((key.clone(), args[1].clone()));
9877 }
9878 Ok(VmValue::Map(Box::new(new_pairs)))
9879 } else {
9880 Err(runtime_err("set() expects a string key"))
9881 }
9882 }
9883 "is_empty" => Ok(VmValue::Bool(pairs.is_empty())),
9884 _ => Err(runtime_err(format!("No method '{}' on map", method))),
9885 }
9886 }
9887
9888 fn dispatch_set_method(
9890 &self,
9891 items: Vec<VmValue>,
9892 method: &str,
9893 args: &[VmValue],
9894 ) -> Result<VmValue, TlError> {
9895 match method {
9896 "len" => Ok(VmValue::Int(items.len() as i64)),
9897 "contains" => {
9898 if args.is_empty() {
9899 return Err(runtime_err("contains() expects a value"));
9900 }
9901 Ok(VmValue::Bool(
9902 items.iter().any(|x| vm_values_equal(x, &args[0])),
9903 ))
9904 }
9905 "add" => {
9906 if args.is_empty() {
9907 return Err(runtime_err("add() expects a value"));
9908 }
9909 let mut new_items = items;
9910 if !new_items.iter().any(|x| vm_values_equal(x, &args[0])) {
9911 new_items.push(args[0].clone());
9912 }
9913 Ok(VmValue::Set(Box::new(new_items)))
9914 }
9915 "remove" => {
9916 if args.is_empty() {
9917 return Err(runtime_err("remove() expects a value"));
9918 }
9919 let new_items: Vec<VmValue> = items
9920 .into_iter()
9921 .filter(|x| !vm_values_equal(x, &args[0]))
9922 .collect();
9923 Ok(VmValue::Set(Box::new(new_items)))
9924 }
9925 "to_list" => Ok(VmValue::List(Box::new(items))),
9926 "union" => {
9927 if args.is_empty() {
9928 return Err(runtime_err("union() expects a set"));
9929 }
9930 if let VmValue::Set(b) = &args[0] {
9931 let mut result = items;
9932 for item in b.iter() {
9933 if !result.iter().any(|x| vm_values_equal(x, item)) {
9934 result.push(item.clone());
9935 }
9936 }
9937 Ok(VmValue::Set(Box::new(result)))
9938 } else {
9939 Err(runtime_err("union() expects a set"))
9940 }
9941 }
9942 "intersection" => {
9943 if args.is_empty() {
9944 return Err(runtime_err("intersection() expects a set"));
9945 }
9946 if let VmValue::Set(b) = &args[0] {
9947 let result: Vec<VmValue> = items
9948 .into_iter()
9949 .filter(|x| b.iter().any(|y| vm_values_equal(x, y)))
9950 .collect();
9951 Ok(VmValue::Set(Box::new(result)))
9952 } else {
9953 Err(runtime_err("intersection() expects a set"))
9954 }
9955 }
9956 "difference" => {
9957 if args.is_empty() {
9958 return Err(runtime_err("difference() expects a set"));
9959 }
9960 if let VmValue::Set(b) = &args[0] {
9961 let result: Vec<VmValue> = items
9962 .into_iter()
9963 .filter(|x| !b.iter().any(|y| vm_values_equal(x, y)))
9964 .collect();
9965 Ok(VmValue::Set(Box::new(result)))
9966 } else {
9967 Err(runtime_err("difference() expects a set"))
9968 }
9969 }
9970 _ => Err(runtime_err(format!("No method '{}' on set", method))),
9971 }
9972 }
9973
9974 #[cfg(feature = "native")]
9976 fn handle_import(&mut self, path: &str, alias: &str) -> Result<VmValue, TlError> {
9977 let resolved = if let Some(ref base) = self.file_path {
9979 let base_dir = std::path::Path::new(base)
9980 .parent()
9981 .unwrap_or(std::path::Path::new("."));
9982 let candidate = base_dir.join(path);
9983 if candidate.exists() {
9984 candidate.to_string_lossy().to_string()
9985 } else {
9986 path.to_string()
9987 }
9988 } else {
9989 path.to_string()
9990 };
9991
9992 if self.importing_files.contains(&resolved) {
9994 return Err(runtime_err(format!("Circular import detected: {resolved}")));
9995 }
9996
9997 if let Some(exports) = self.module_cache.get(&resolved) {
9999 let exports = exports.clone();
10000 return self.bind_import_exports(exports, alias);
10001 }
10002
10003 let source = std::fs::read_to_string(&resolved)
10005 .map_err(|e| runtime_err(format!("Cannot import '{}': {}", resolved, e)))?;
10006 let program = tl_parser::parse(&source)
10007 .map_err(|e| runtime_err(format!("Parse error in '{}': {}", resolved, e)))?;
10008 let proto = crate::compiler::compile(&program)
10009 .map_err(|e| runtime_err(format!("Compile error in '{}': {}", resolved, e)))?;
10010
10011 self.importing_files.insert(resolved.clone());
10013
10014 let mut import_vm = Vm::new();
10016 import_vm.file_path = Some(resolved.clone());
10017 import_vm.globals = self.globals.clone();
10018 import_vm.importing_files = self.importing_files.clone();
10019 import_vm.module_cache = self.module_cache.clone();
10020 import_vm.package_roots = self.package_roots.clone();
10021 import_vm.project_root = self.project_root.clone();
10022 import_vm.execute(&proto)?;
10023
10024 self.importing_files.remove(&resolved);
10025
10026 let mut exports = HashMap::new();
10028
10029 for (k, v) in &import_vm.globals {
10031 if !self.globals.contains_key(k) {
10032 exports.insert(k.clone(), v.clone());
10033 }
10034 }
10035
10036 for (name, reg) in &proto.top_level_locals {
10038 if !name.starts_with("__enum_") && !exports.contains_key(name) {
10039 let stack_idx = reg;
10040 if (*stack_idx as usize) < import_vm.stack.len() {
10041 let val = import_vm.stack[*stack_idx as usize].clone();
10042 if !matches!(val, VmValue::None) || name.starts_with("_") {
10043 exports.insert(name.clone(), val);
10044 }
10045 }
10046 }
10047 }
10048
10049 self.module_cache.insert(resolved, exports.clone());
10051 for (k, v) in import_vm.module_cache {
10053 self.module_cache.entry(k).or_insert(v);
10054 }
10055
10056 self.bind_import_exports(exports, alias)
10057 }
10058
10059 #[cfg(feature = "native")]
10061 fn bind_import_exports(
10062 &mut self,
10063 exports: HashMap<String, VmValue>,
10064 alias: &str,
10065 ) -> Result<VmValue, TlError> {
10066 if alias.is_empty() {
10067 for (k, v) in &exports {
10069 self.globals.insert(k.clone(), v.clone());
10070 }
10071 Ok(VmValue::None)
10072 } else {
10073 let module = VmModule {
10075 name: Arc::from(alias),
10076 exports,
10077 };
10078 let module_val = VmValue::Module(Arc::new(module));
10079 self.globals.insert(alias.to_string(), module_val.clone());
10080 Ok(module_val)
10081 }
10082 }
10083
10084 #[cfg(feature = "native")]
10086 fn handle_use_import(
10087 &mut self,
10088 path_str: &str,
10089 extra_a: u8,
10090 kind: u8,
10091 _frame_idx: usize,
10092 ) -> Result<VmValue, TlError> {
10093 match kind {
10094 0 => {
10095 let segments: Vec<&str> = path_str.split('.').collect();
10097 let file_path = self.resolve_use_path(&segments)?;
10098 let _last = segments.last().copied().unwrap_or("");
10100 self.handle_import(&file_path, "")?;
10101 Ok(VmValue::None)
10106 }
10107 1 => {
10108 let brace_start = path_str.find('{').unwrap_or(path_str.len());
10110 let prefix = path_str[..brace_start].trim_end_matches('.');
10111 let segments: Vec<&str> = prefix.split('.').collect();
10112 let file_path = self.resolve_use_path(&segments)?;
10113 self.handle_import(&file_path, "")?;
10114 Ok(VmValue::None)
10115 }
10116 2 => {
10117 let prefix = path_str.trim_end_matches(".*");
10119 let segments: Vec<&str> = prefix.split('.').collect();
10120 let file_path = self.resolve_use_path(&segments)?;
10121 self.handle_import(&file_path, "")?;
10122 Ok(VmValue::None)
10123 }
10124 3 => {
10125 let segments: Vec<&str> = path_str.split('.').collect();
10127 let file_path = self.resolve_use_path(&segments)?;
10128 let alias_str = if let Some(frame) = self.frames.last() {
10131 if let Some(crate::chunk::Constant::String(s)) =
10132 frame.prototype.constants.get(extra_a as usize)
10133 {
10134 s.to_string()
10135 } else {
10136 segments.last().copied().unwrap_or("module").to_string()
10137 }
10138 } else {
10139 segments.last().copied().unwrap_or("module").to_string()
10140 };
10141 self.handle_import(&file_path, &alias_str)?;
10142 Ok(VmValue::None)
10143 }
10144 _ => Err(runtime_err(format!("Unknown use-import kind: {kind}"))),
10145 }
10146 }
10147
10148 #[cfg(feature = "native")]
10150 fn resolve_use_path(&self, segments: &[&str]) -> Result<String, TlError> {
10151 if segments.contains(&"..") {
10153 return Err(runtime_err("Import paths cannot contain '..'"));
10154 }
10155
10156 let base_dir = if let Some(ref fp) = self.file_path {
10157 std::path::Path::new(fp)
10158 .parent()
10159 .unwrap_or(std::path::Path::new("."))
10160 .to_path_buf()
10161 } else {
10162 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
10163 };
10164
10165 let rel_path = segments.join("/");
10166
10167 let file_path = base_dir.join(format!("{rel_path}.tl"));
10169 if file_path.exists() {
10170 return Ok(file_path.to_string_lossy().to_string());
10171 }
10172
10173 let dir_path = base_dir.join(&rel_path).join("mod.tl");
10175 if dir_path.exists() {
10176 return Ok(dir_path.to_string_lossy().to_string());
10177 }
10178
10179 if segments.len() > 1 {
10181 let parent = &segments[..segments.len() - 1];
10182 let parent_path = parent.join("/");
10183 let parent_file = base_dir.join(format!("{parent_path}.tl"));
10184 if parent_file.exists() {
10185 return Ok(parent_file.to_string_lossy().to_string());
10186 }
10187 let parent_dir = base_dir.join(&parent_path).join("mod.tl");
10188 if parent_dir.exists() {
10189 return Ok(parent_dir.to_string_lossy().to_string());
10190 }
10191 }
10192
10193 let pkg_name_underscore = segments[0];
10196 let pkg_name_hyphen = pkg_name_underscore.replace('_', "-");
10197 let pkg_root = self
10198 .package_roots
10199 .get(pkg_name_underscore)
10200 .or_else(|| self.package_roots.get(&pkg_name_hyphen));
10201
10202 if let Some(root) = pkg_root {
10203 let remaining = &segments[1..];
10204 if let Some(path) = resolve_package_file(root, remaining) {
10205 return Ok(path);
10206 }
10207 }
10208
10209 Err(runtime_err(format!(
10210 "Module not found: `{}`",
10211 segments.join(".")
10212 )))
10213 }
10214
10215 fn call_vm_function(&mut self, func: &VmValue, args: &[VmValue]) -> Result<VmValue, TlError> {
10217 match func {
10218 VmValue::Function(closure) => {
10219 let proto = closure.prototype.clone();
10220 let arity = proto.arity as usize;
10221 if args.len() != arity {
10222 return Err(runtime_err(format!(
10223 "Expected {} arguments, got {}",
10224 arity,
10225 args.len()
10226 )));
10227 }
10228
10229 if proto.is_generator {
10231 let mut closed_upvalues = Vec::new();
10232 for uv in &closure.upvalues {
10233 match uv {
10234 UpvalueRef::Open { stack_index } => {
10235 let val = self.stack[*stack_index].clone();
10236 closed_upvalues.push(UpvalueRef::Closed(val));
10237 }
10238 UpvalueRef::Closed(v) => {
10239 closed_upvalues.push(UpvalueRef::Closed(v.clone()));
10240 }
10241 }
10242 }
10243 let num_regs = proto.num_registers as usize;
10244 let mut saved_stack = vec![VmValue::None; num_regs];
10245 for (i, arg) in args.iter().enumerate() {
10246 saved_stack[i] = arg.clone();
10247 }
10248 let gn = VmGenerator::new(GeneratorKind::UserDefined {
10249 prototype: proto,
10250 upvalues: closed_upvalues,
10251 saved_stack,
10252 ip: 0,
10253 });
10254 return Ok(VmValue::Generator(Arc::new(Mutex::new(gn))));
10255 }
10256
10257 let new_base = self.stack.len();
10258 self.ensure_stack(new_base + proto.num_registers as usize + 1);
10259
10260 for (i, arg) in args.iter().enumerate() {
10261 self.stack[new_base + i] = arg.clone();
10262 }
10263
10264 self.frames.push(CallFrame {
10265 prototype: proto,
10266 ip: 0,
10267 base: new_base,
10268 upvalues: closure.upvalues.clone(),
10269 });
10270
10271 let result = self.run()?;
10272 self.stack.truncate(new_base);
10273 Ok(result)
10274 }
10275 VmValue::Builtin(id) => {
10276 let args_base = self.stack.len();
10278 for arg in args {
10279 self.stack.push(arg.clone());
10280 }
10281 let result = self.call_builtin(*id as u16, args_base, args.len());
10282 self.stack.truncate(args_base);
10283 result
10284 }
10285 _ => Err(runtime_err(format!("Cannot call {}", func.type_name()))),
10286 }
10287 }
10288
10289 #[cfg(feature = "native")]
10292 fn handle_table_pipe(
10293 &mut self,
10294 frame_idx: usize,
10295 table_val: VmValue,
10296 op_const: u8,
10297 args_const: u8,
10298 ) -> Result<VmValue, TlError> {
10299 let df = match table_val {
10300 VmValue::Table(t) => t.df,
10301 other => {
10302 return self.table_pipe_fallback(other, frame_idx, op_const, args_const);
10304 }
10305 };
10306
10307 let frame = &self.frames[frame_idx];
10308 let op_name = match &frame.prototype.constants[op_const as usize] {
10309 Constant::String(s) => s.to_string(),
10310 _ => return Err(runtime_err("Expected string constant for table op")),
10311 };
10312 let ast_args = match &frame.prototype.constants[args_const as usize] {
10313 Constant::AstExprList(args) => args.clone(),
10314 _ => return Err(runtime_err("Expected AST expr list for table args")),
10315 };
10316
10317 let ctx = self.build_translate_context();
10318
10319 match op_name.as_str() {
10320 "filter" => {
10321 if ast_args.len() != 1 {
10322 return Err(runtime_err("filter() expects 1 argument (predicate)"));
10323 }
10324 let pred = translate_expr(&ast_args[0], &ctx).map_err(runtime_err)?;
10325 let filtered = df.filter(pred).map_err(|e| runtime_err(format!("{e}")))?;
10326 Ok(VmValue::Table(VmTable { df: filtered }))
10327 }
10328 "select" => {
10329 if ast_args.is_empty() {
10330 return Err(runtime_err("select() expects at least 1 argument"));
10331 }
10332 let mut select_exprs = Vec::new();
10333 for arg in &ast_args {
10334 match arg {
10335 AstExpr::Ident(name) => select_exprs.push(col(name.as_str())),
10336 AstExpr::NamedArg { name, value } => {
10337 let expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10338 select_exprs.push(expr.alias(name));
10339 }
10340 AstExpr::String(name) => select_exprs.push(col(name.as_str())),
10341 other => {
10342 let expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10343 select_exprs.push(expr);
10344 }
10345 }
10346 }
10347 let selected = df
10348 .select(select_exprs)
10349 .map_err(|e| runtime_err(format!("{e}")))?;
10350 Ok(VmValue::Table(VmTable { df: selected }))
10351 }
10352 "sort" => {
10353 if ast_args.is_empty() {
10354 return Err(runtime_err("sort() expects at least 1 argument (column)"));
10355 }
10356 let mut sort_exprs = Vec::new();
10357 let mut i = 0;
10358 while i < ast_args.len() {
10359 let col_name = match &ast_args[i] {
10360 AstExpr::Ident(name) => name.clone(),
10361 AstExpr::String(name) => name.clone(),
10362 _ => {
10363 return Err(runtime_err(
10364 "sort() column must be an identifier or string",
10365 ));
10366 }
10367 };
10368 i += 1;
10369 let ascending = if i < ast_args.len() {
10370 match &ast_args[i] {
10371 AstExpr::String(dir) if dir == "desc" || dir == "DESC" => {
10372 i += 1;
10373 false
10374 }
10375 AstExpr::String(dir) if dir == "asc" || dir == "ASC" => {
10376 i += 1;
10377 true
10378 }
10379 _ => true,
10380 }
10381 } else {
10382 true
10383 };
10384 sort_exprs.push(col(col_name.as_str()).sort(ascending, true));
10385 }
10386 let sorted = df
10387 .sort(sort_exprs)
10388 .map_err(|e| runtime_err(format!("{e}")))?;
10389 Ok(VmValue::Table(VmTable { df: sorted }))
10390 }
10391 "with" => {
10392 if ast_args.len() != 1 {
10393 return Err(runtime_err(
10394 "with() expects 1 argument (map of column definitions)",
10395 ));
10396 }
10397 let pairs = match &ast_args[0] {
10398 AstExpr::Map(pairs) => pairs,
10399 _ => return Err(runtime_err("with() expects a map { col = expr, ... }")),
10400 };
10401 let mut result_df = df;
10402 for (key, value_expr) in pairs {
10403 let col_name = match key {
10404 AstExpr::String(s) => s.clone(),
10405 AstExpr::Ident(s) => s.clone(),
10406 _ => return Err(runtime_err("with() key must be a string or identifier")),
10407 };
10408 let df_expr = translate_expr(value_expr, &ctx).map_err(runtime_err)?;
10409 result_df = result_df
10410 .with_column(&col_name, df_expr)
10411 .map_err(|e| runtime_err(format!("{e}")))?;
10412 }
10413 Ok(VmValue::Table(VmTable { df: result_df }))
10414 }
10415 "aggregate" => {
10416 let mut group_by_cols: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10417 let mut agg_exprs: Vec<tl_data::datafusion::prelude::Expr> = Vec::new();
10418 for arg in &ast_args {
10419 match arg {
10420 AstExpr::NamedArg { name, value } if name == "by" => match value.as_ref() {
10421 AstExpr::String(col_name) => group_by_cols.push(col(col_name.as_str())),
10422 AstExpr::Ident(col_name) => group_by_cols.push(col(col_name.as_str())),
10423 AstExpr::List(items) => {
10424 for item in items {
10425 match item {
10426 AstExpr::String(s) => group_by_cols.push(col(s.as_str())),
10427 AstExpr::Ident(s) => group_by_cols.push(col(s.as_str())),
10428 _ => {
10429 return Err(runtime_err(
10430 "by: list items must be strings or identifiers",
10431 ));
10432 }
10433 }
10434 }
10435 }
10436 _ => return Err(runtime_err("by: must be a column name or list")),
10437 },
10438 AstExpr::NamedArg { name, value } => {
10439 let agg_expr = translate_expr(value, &ctx).map_err(runtime_err)?;
10440 agg_exprs.push(agg_expr.alias(name));
10441 }
10442 other => {
10443 let agg_expr = translate_expr(other, &ctx).map_err(runtime_err)?;
10444 agg_exprs.push(agg_expr);
10445 }
10446 }
10447 }
10448 let aggregated = df
10449 .aggregate(group_by_cols, agg_exprs)
10450 .map_err(|e| runtime_err(format!("{e}")))?;
10451 Ok(VmValue::Table(VmTable { df: aggregated }))
10452 }
10453 "join" => {
10454 if ast_args.is_empty() {
10455 return Err(runtime_err(
10456 "join() expects at least 1 argument (right table)",
10457 ));
10458 }
10459 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10461 let right_df = match right_table {
10462 VmValue::Table(t) => t.df,
10463 _ => return Err(runtime_err("join() first arg must be a table")),
10464 };
10465 let mut left_cols: Vec<String> = Vec::new();
10466 let mut right_cols: Vec<String> = Vec::new();
10467 let mut join_type = JoinType::Inner;
10468 for arg in &ast_args[1..] {
10469 match arg {
10470 AstExpr::NamedArg { name, value } if name == "on" => {
10471 if let AstExpr::BinOp {
10472 left,
10473 op: tl_ast::BinOp::Eq,
10474 right,
10475 } = value.as_ref()
10476 {
10477 let lc = match left.as_ref() {
10478 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10479 _ => {
10480 return Err(runtime_err(
10481 "on: left side must be a column name",
10482 ));
10483 }
10484 };
10485 let rc = match right.as_ref() {
10486 AstExpr::Ident(s) | AstExpr::String(s) => s.clone(),
10487 _ => {
10488 return Err(runtime_err(
10489 "on: right side must be a column name",
10490 ));
10491 }
10492 };
10493 left_cols.push(lc);
10494 right_cols.push(rc);
10495 }
10496 }
10497 AstExpr::NamedArg { name, value } if name == "kind" => {
10498 if let AstExpr::String(kind_str) = value.as_ref() {
10499 join_type = match kind_str.as_str() {
10500 "inner" => JoinType::Inner,
10501 "left" => JoinType::Left,
10502 "right" => JoinType::Right,
10503 "full" => JoinType::Full,
10504 _ => {
10505 return Err(runtime_err(format!(
10506 "Unknown join type: {kind_str}"
10507 )));
10508 }
10509 };
10510 }
10511 }
10512 _ => {}
10513 }
10514 }
10515 let lc_refs: Vec<&str> = left_cols.iter().map(|s| s.as_str()).collect();
10516 let rc_refs: Vec<&str> = right_cols.iter().map(|s| s.as_str()).collect();
10517 let joined = df
10518 .join(right_df, join_type, &lc_refs, &rc_refs, None)
10519 .map_err(|e| runtime_err(format!("{e}")))?;
10520 Ok(VmValue::Table(VmTable { df: joined }))
10521 }
10522 "head" | "limit" => {
10523 let n = match ast_args.first() {
10524 Some(AstExpr::Int(n)) => *n as usize,
10525 None => 10,
10526 _ => return Err(runtime_err("head/limit expects an integer")),
10527 };
10528 let limited = df
10529 .limit(0, Some(n))
10530 .map_err(|e| runtime_err(format!("{e}")))?;
10531 Ok(VmValue::Table(VmTable { df: limited }))
10532 }
10533 "collect" => {
10534 let batches = self.engine().collect(df).map_err(runtime_err)?;
10535 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10536 Ok(VmValue::String(Arc::from(formatted.as_str())))
10537 }
10538 "show" => {
10539 let limit = match ast_args.first() {
10540 Some(AstExpr::Int(n)) => *n as usize,
10541 None => 20,
10542 _ => 20,
10543 };
10544 let limited = df
10545 .limit(0, Some(limit))
10546 .map_err(|e| runtime_err(format!("{e}")))?;
10547 let batches = self.engine().collect(limited).map_err(runtime_err)?;
10548 let formatted = DataEngine::format_batches(&batches).map_err(runtime_err)?;
10549 println!("{formatted}");
10550 self.output.push(formatted);
10551 Ok(VmValue::None)
10552 }
10553 "describe" => {
10554 let schema = df.schema();
10555 let mut lines = Vec::new();
10556 lines.push("Columns:".to_string());
10557 for field in schema.fields() {
10558 lines.push(format!(" {}: {}", field.name(), field.data_type()));
10559 }
10560 let output = lines.join("\n");
10561 println!("{output}");
10562 self.output.push(output.clone());
10563 Ok(VmValue::String(Arc::from(output.as_str())))
10564 }
10565 "write_csv" => {
10566 if ast_args.len() != 1 {
10567 return Err(runtime_err("write_csv() expects 1 argument (path)"));
10568 }
10569 let path = self.eval_ast_to_string(&ast_args[0])?;
10570 self.engine().write_csv(df, &path).map_err(runtime_err)?;
10571 Ok(VmValue::None)
10572 }
10573 "write_parquet" => {
10574 if ast_args.len() != 1 {
10575 return Err(runtime_err("write_parquet() expects 1 argument (path)"));
10576 }
10577 let path = self.eval_ast_to_string(&ast_args[0])?;
10578 self.engine()
10579 .write_parquet(df, &path)
10580 .map_err(runtime_err)?;
10581 Ok(VmValue::None)
10582 }
10583 "fill_null" => {
10585 if ast_args.is_empty() {
10586 return Err(runtime_err(
10587 "fill_null() expects (column, [strategy/value])",
10588 ));
10589 }
10590 let column = self.eval_ast_to_string(&ast_args[0])?;
10591 if ast_args.len() >= 2 {
10592 let val = self.eval_ast_to_vm(&ast_args[1])?;
10593 match val {
10594 VmValue::String(s) => {
10595 let fill_val = if ast_args.len() >= 3 {
10597 match self.eval_ast_to_vm(&ast_args[2])? {
10598 VmValue::Int(n) => Some(n as f64),
10599 VmValue::Float(f) => Some(f),
10600 _ => None,
10601 }
10602 } else {
10603 None
10604 };
10605 let result = self
10606 .engine()
10607 .fill_null(df, &column, &s, fill_val)
10608 .map_err(runtime_err)?;
10609 Ok(VmValue::Table(VmTable { df: result }))
10610 }
10611 VmValue::Int(n) => {
10612 let result = self
10613 .engine()
10614 .fill_null(df, &column, "value", Some(n as f64))
10615 .map_err(runtime_err)?;
10616 Ok(VmValue::Table(VmTable { df: result }))
10617 }
10618 VmValue::Float(f) => {
10619 let result = self
10620 .engine()
10621 .fill_null(df, &column, "value", Some(f))
10622 .map_err(runtime_err)?;
10623 Ok(VmValue::Table(VmTable { df: result }))
10624 }
10625 _ => Err(runtime_err(
10626 "fill_null() second arg must be a strategy or fill value",
10627 )),
10628 }
10629 } else {
10630 let result = self
10631 .engine()
10632 .fill_null(df, &column, "zero", None)
10633 .map_err(runtime_err)?;
10634 Ok(VmValue::Table(VmTable { df: result }))
10635 }
10636 }
10637 "drop_null" => {
10638 if ast_args.is_empty() {
10639 return Err(runtime_err("drop_null() expects (column)"));
10640 }
10641 let column = self.eval_ast_to_string(&ast_args[0])?;
10642 let result = self.engine().drop_null(df, &column).map_err(runtime_err)?;
10643 Ok(VmValue::Table(VmTable { df: result }))
10644 }
10645 "dedup" => {
10646 let columns: Vec<String> = ast_args
10647 .iter()
10648 .filter_map(|a| self.eval_ast_to_string(a).ok())
10649 .collect();
10650 let result = self.engine().dedup(df, &columns).map_err(runtime_err)?;
10651 Ok(VmValue::Table(VmTable { df: result }))
10652 }
10653 "clamp" => {
10654 if ast_args.len() < 3 {
10655 return Err(runtime_err("clamp() expects (column, min, max)"));
10656 }
10657 let column = self.eval_ast_to_string(&ast_args[0])?;
10658 let min_val = match self.eval_ast_to_vm(&ast_args[1])? {
10659 VmValue::Int(n) => n as f64,
10660 VmValue::Float(f) => f,
10661 _ => return Err(runtime_err("clamp() min must be a number")),
10662 };
10663 let max_val = match self.eval_ast_to_vm(&ast_args[2])? {
10664 VmValue::Int(n) => n as f64,
10665 VmValue::Float(f) => f,
10666 _ => return Err(runtime_err("clamp() max must be a number")),
10667 };
10668 let result = self
10669 .engine()
10670 .clamp(df, &column, min_val, max_val)
10671 .map_err(runtime_err)?;
10672 Ok(VmValue::Table(VmTable { df: result }))
10673 }
10674 "data_profile" => {
10675 let result = self.engine().data_profile(df).map_err(runtime_err)?;
10676 Ok(VmValue::Table(VmTable { df: result }))
10677 }
10678 "row_count" => {
10679 let count = self.engine().row_count(df).map_err(runtime_err)?;
10680 Ok(VmValue::Int(count))
10681 }
10682 "null_rate" => {
10683 if ast_args.is_empty() {
10684 return Err(runtime_err("null_rate() expects (column)"));
10685 }
10686 let column = self.eval_ast_to_string(&ast_args[0])?;
10687 let rate = self.engine().null_rate(df, &column).map_err(runtime_err)?;
10688 Ok(VmValue::Float(rate))
10689 }
10690 "is_unique" => {
10691 if ast_args.is_empty() {
10692 return Err(runtime_err("is_unique() expects (column)"));
10693 }
10694 let column = self.eval_ast_to_string(&ast_args[0])?;
10695 let unique = self.engine().is_unique(df, &column).map_err(runtime_err)?;
10696 Ok(VmValue::Bool(unique))
10697 }
10698 "window" => {
10700 use tl_data::datafusion::logical_expr::{
10701 WindowFrame, WindowFunctionDefinition,
10702 expr::{Sort as DfSort, WindowFunction as WinFunc},
10703 };
10704 if ast_args.is_empty() {
10705 return Err(runtime_err(
10706 "window() expects named arguments: fn, partition_by, order_by, alias",
10707 ));
10708 }
10709 let mut win_fn_name = String::new();
10710 let mut partition_by_cols: Vec<String> = Vec::new();
10711 let mut order_by_cols: Vec<String> = Vec::new();
10712 let mut alias_name = String::new();
10713 let mut win_args: Vec<String> = Vec::new();
10714 let mut descending = false;
10715
10716 for arg in &ast_args {
10717 if let AstExpr::NamedArg { name, value } = arg {
10718 match name.as_str() {
10719 "fn" => win_fn_name = self.eval_ast_to_string(value)?,
10720 "partition_by" => match value.as_ref() {
10721 AstExpr::List(items) => {
10722 for item in items {
10723 partition_by_cols.push(self.eval_ast_to_string(item)?);
10724 }
10725 }
10726 _ => partition_by_cols.push(self.eval_ast_to_string(value)?),
10727 },
10728 "order_by" => match value.as_ref() {
10729 AstExpr::List(items) => {
10730 for item in items {
10731 order_by_cols.push(self.eval_ast_to_string(item)?);
10732 }
10733 }
10734 _ => order_by_cols.push(self.eval_ast_to_string(value)?),
10735 },
10736 "alias" | "as" => alias_name = self.eval_ast_to_string(value)?,
10737 "args" => match value.as_ref() {
10738 AstExpr::List(items) => {
10739 for item in items {
10740 win_args.push(self.eval_ast_to_string(item)?);
10741 }
10742 }
10743 _ => win_args.push(self.eval_ast_to_string(value)?),
10744 },
10745 "desc" => {
10746 if let AstExpr::Bool(b) = value.as_ref() {
10747 descending = *b;
10748 }
10749 }
10750 _ => {}
10751 }
10752 }
10753 }
10754
10755 if win_fn_name.is_empty() {
10756 return Err(runtime_err(
10757 "window() requires fn: parameter (rank, row_number, dense_rank, lag, lead, ntile)",
10758 ));
10759 }
10760 if alias_name.is_empty() {
10761 alias_name = win_fn_name.clone();
10762 }
10763
10764 let session = self.engine().session_ctx();
10766 let win_udf = match win_fn_name.as_str() {
10767 "rank" => session.udwf("rank"),
10768 "dense_rank" => session.udwf("dense_rank"),
10769 "row_number" => session.udwf("row_number"),
10770 "percent_rank" => session.udwf("percent_rank"),
10771 "cume_dist" => session.udwf("cume_dist"),
10772 "ntile" => session.udwf("ntile"),
10773 "lag" => session.udwf("lag"),
10774 "lead" => session.udwf("lead"),
10775 "first_value" => session.udwf("first_value"),
10776 "last_value" => session.udwf("last_value"),
10777 _ => {
10778 return Err(runtime_err(format!(
10779 "Unknown window function: {win_fn_name}"
10780 )));
10781 }
10782 }
10783 .map_err(|e| {
10784 runtime_err(format!(
10785 "Window function '{win_fn_name}' not available: {e}"
10786 ))
10787 })?;
10788
10789 let fun = WindowFunctionDefinition::WindowUDF(win_udf);
10790
10791 let func_args: Vec<tl_data::datafusion::prelude::Expr> = win_args
10793 .iter()
10794 .map(|a| {
10795 if let Ok(n) = a.parse::<i64>() {
10796 lit(n)
10797 } else {
10798 col(a.as_str())
10799 }
10800 })
10801 .collect();
10802
10803 let partition_exprs: Vec<tl_data::datafusion::prelude::Expr> =
10804 partition_by_cols.iter().map(|c| col(c.as_str())).collect();
10805 let order_exprs: Vec<DfSort> = order_by_cols
10806 .iter()
10807 .map(|c| DfSort::new(col(c.as_str()), !descending, true))
10808 .collect();
10809
10810 let has_order = !order_exprs.is_empty();
10811 let win_expr = tl_data::datafusion::prelude::Expr::WindowFunction(WinFunc {
10812 fun,
10813 args: func_args,
10814 partition_by: partition_exprs,
10815 order_by: order_exprs,
10816 window_frame: WindowFrame::new(if has_order { Some(true) } else { None }),
10817 null_treatment: None,
10818 })
10819 .alias(&alias_name);
10820
10821 let schema = df.schema();
10823 let mut select_exprs: Vec<tl_data::datafusion::prelude::Expr> = schema
10824 .fields()
10825 .iter()
10826 .map(|f| col(f.name().as_str()))
10827 .collect();
10828 select_exprs.push(win_expr);
10829
10830 let result_df = df
10831 .select(select_exprs)
10832 .map_err(|e| runtime_err(format!("Window function error: {e}")))?;
10833 Ok(VmValue::Table(VmTable { df: result_df }))
10834 }
10835 "union" => {
10837 if ast_args.is_empty() {
10838 return Err(runtime_err("union() expects a table argument"));
10839 }
10840 let right_table = self.eval_ast_to_vm(&ast_args[0])?;
10841 let right_df = match right_table {
10842 VmValue::Table(t) => t.df,
10843 _ => return Err(runtime_err("union() argument must be a table")),
10844 };
10845 let result_df = df
10846 .union(right_df)
10847 .map_err(|e| runtime_err(format!("Union error: {e}")))?;
10848 Ok(VmValue::Table(VmTable { df: result_df }))
10849 }
10850 "sample" => {
10852 use tl_data::datafusion::arrow::{array::UInt32Array, compute};
10853 use tl_data::datafusion::datasource::MemTable;
10854 if ast_args.is_empty() {
10855 return Err(runtime_err("sample() expects a count or fraction"));
10856 }
10857 let batches = self.engine().collect(df).map_err(runtime_err)?;
10858 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
10859 let sample_count = match &ast_args[0] {
10860 AstExpr::Int(n) => (*n as usize).min(total_rows),
10861 AstExpr::Float(f) if *f > 0.0 && *f <= 1.0 => {
10862 ((total_rows as f64) * f).ceil() as usize
10863 }
10864 _ => {
10865 let val = self.eval_ast_to_string(&ast_args[0])?;
10866 val.parse::<usize>().map_err(|_| {
10867 runtime_err("sample() expects integer count or float fraction")
10868 })?
10869 }
10870 };
10871 if total_rows == 0 || sample_count == 0 {
10872 let schema = batches[0].schema();
10873 let empty = tl_data::datafusion::arrow::record_batch::RecordBatch::new_empty(
10874 schema.clone(),
10875 );
10876 let mem_table = MemTable::try_new(schema, vec![vec![empty]])
10877 .map_err(|e| runtime_err(format!("{e}")))?;
10878 let new_df = self
10879 .engine()
10880 .session_ctx()
10881 .read_table(Arc::new(mem_table))
10882 .map_err(|e| runtime_err(format!("{e}")))?;
10883 return Ok(VmValue::Table(VmTable { df: new_df }));
10884 }
10885 let mut rng = rand::thread_rng();
10887 let mut indices: Vec<usize> = (0..total_rows).collect();
10888 use rand::seq::SliceRandom;
10889 indices.partial_shuffle(&mut rng, sample_count);
10890 indices.truncate(sample_count);
10891 indices.sort();
10892 let combined = compute::concat_batches(&batches[0].schema(), &batches)
10894 .map_err(|e| runtime_err(format!("{e}")))?;
10895 let idx_array =
10896 UInt32Array::from(indices.iter().map(|&i| i as u32).collect::<Vec<_>>());
10897 let sampled_cols: Vec<tl_data::datafusion::arrow::array::ArrayRef> = (0..combined
10898 .num_columns())
10899 .map(|c| {
10900 compute::take(combined.column(c), &idx_array, None)
10901 .map_err(|e| runtime_err(format!("{e}")))
10902 })
10903 .collect::<Result<Vec<_>, _>>()?;
10904 let sampled_batch = tl_data::datafusion::arrow::record_batch::RecordBatch::try_new(
10905 combined.schema(),
10906 sampled_cols,
10907 )
10908 .map_err(|e| runtime_err(format!("{e}")))?;
10909 let mem_table =
10910 MemTable::try_new(sampled_batch.schema(), vec![vec![sampled_batch]])
10911 .map_err(|e| runtime_err(format!("{e}")))?;
10912 let new_df = self
10913 .engine()
10914 .session_ctx()
10915 .read_table(Arc::new(mem_table))
10916 .map_err(|e| runtime_err(format!("{e}")))?;
10917 Ok(VmValue::Table(VmTable { df: new_df }))
10918 }
10919 _ => Err(runtime_err(format!("Unknown table operation: {op_name}"))),
10920 }
10921 }
10922
10923 fn table_pipe_fallback(
10926 &mut self,
10927 left_val: VmValue,
10928 frame_idx: usize,
10929 op_const: u8,
10930 args_const: u8,
10931 ) -> Result<VmValue, TlError> {
10932 let frame = &self.frames[frame_idx];
10933 let op_name = match &frame.prototype.constants[op_const as usize] {
10934 Constant::String(s) => s.to_string(),
10935 _ => return Err(runtime_err("Expected string constant for table op")),
10936 };
10937 let ast_args = match &frame.prototype.constants[args_const as usize] {
10938 Constant::AstExprList(args) => args.clone(),
10939 _ => return Err(runtime_err("Expected AST expr list for table args")),
10940 };
10941
10942 if let Some(builtin_id) = BuiltinId::from_name(&op_name) {
10944 let mut all_args = vec![left_val];
10946 for arg in &ast_args {
10947 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10948 }
10949 let args_base = self.stack.len();
10950 for arg in &all_args {
10951 self.stack.push(arg.clone());
10952 }
10953 let result = self.call_builtin(builtin_id as u16, args_base, all_args.len());
10954 self.stack.truncate(args_base);
10955 return result;
10956 }
10957
10958 if let Some(func) = self.globals.get(&op_name).cloned() {
10960 let mut all_args = vec![left_val];
10961 for arg in &ast_args {
10962 all_args.push(self.eval_ast_to_vm(arg).unwrap_or(VmValue::None));
10963 }
10964 return self.call_vm_function(&func, &all_args);
10965 }
10966
10967 Err(runtime_err(format!("Unknown operation: `{op_name}`")))
10968 }
10969
10970 #[cfg(feature = "native")]
10972 fn build_translate_context(&self) -> TranslateContext {
10973 let mut ctx = TranslateContext::new();
10974 for (name, val) in &self.globals {
10976 let local = match val {
10977 VmValue::Int(n) => Some(LocalValue::Int(*n)),
10978 VmValue::Float(f) => Some(LocalValue::Float(*f)),
10979 VmValue::String(s) => Some(LocalValue::String(s.to_string())),
10980 VmValue::Bool(b) => Some(LocalValue::Bool(*b)),
10981 _ => None,
10982 };
10983 if let Some(l) = local {
10984 ctx.locals.insert(name.clone(), l);
10985 }
10986 }
10987 if let Some(frame) = self.frames.last() {
10989 for local_idx in 0..frame.prototype.num_locals as usize {
10990 if let Some(val) = self.stack.get(frame.base + local_idx) {
10991 let _ = val;
10993 }
10994 }
10995 }
10996 ctx
10997 }
10998
10999 fn eval_ast_to_vm(&mut self, expr: &AstExpr) -> Result<VmValue, TlError> {
11002 match expr {
11003 AstExpr::Ident(name) => {
11004 if let Some(val) = self.globals.get(name) {
11006 return Ok(val.clone());
11007 }
11008 if let Some(val) = self.lookup_frame_local(name) {
11010 return Ok(val);
11011 }
11012 Err(runtime_err(format!("Undefined variable: `{name}`")))
11013 }
11014 AstExpr::String(s) => Ok(VmValue::String(Arc::from(s.as_str()))),
11015 AstExpr::Int(n) => Ok(VmValue::Int(*n)),
11016 AstExpr::Float(f) => Ok(VmValue::Float(*f)),
11017 AstExpr::Bool(b) => Ok(VmValue::Bool(*b)),
11018 AstExpr::None => Ok(VmValue::None),
11019 AstExpr::Closure {
11020 params: _, body: _, ..
11021 } => {
11022 use crate::compiler;
11023 let wrapper = tl_ast::Program {
11024 statements: vec![tl_ast::Stmt {
11025 kind: tl_ast::StmtKind::Expr(expr.clone()),
11026 span: tl_errors::Span::new(0, 0),
11027 doc_comment: None,
11028 }],
11029 module_doc: None,
11030 };
11031 let proto = compiler::compile(&wrapper)?;
11032 let mut temp_vm = Vm::new();
11033 temp_vm.globals = self.globals.clone();
11036 for (n, v) in self.current_frame_locals() {
11037 temp_vm.globals.insert(n, v);
11038 }
11039 let result = temp_vm.execute(&proto)?;
11040 Ok(result)
11041 }
11042 _ => {
11043 let wrapper = tl_ast::Program {
11045 statements: vec![tl_ast::Stmt {
11046 kind: tl_ast::StmtKind::Expr(expr.clone()),
11047 span: tl_errors::Span::new(0, 0),
11048 doc_comment: None,
11049 }],
11050 module_doc: None,
11051 };
11052 use crate::compiler;
11053 let proto = compiler::compile(&wrapper)?;
11054 let mut temp_vm = Vm::new();
11055 temp_vm.globals = self.globals.clone();
11058 for (n, v) in self.current_frame_locals() {
11059 temp_vm.globals.insert(n, v);
11060 }
11061 temp_vm.execute(&proto)
11062 }
11063 }
11064 }
11065
11066 fn lookup_frame_local(&self, name: &str) -> Option<VmValue> {
11071 let frame = self.frames.last()?;
11072 let mut found = None;
11073 for (local_name, reg) in &frame.prototype.top_level_locals {
11074 if local_name == name {
11075 let idx = frame.base + *reg as usize;
11076 if let Some(v) = self.stack.get(idx)
11077 && !matches!(v, VmValue::None)
11078 {
11079 found = Some(v.clone()); }
11081 }
11082 }
11083 found
11084 }
11085
11086 fn current_frame_locals(&self) -> Vec<(String, VmValue)> {
11088 let mut out = Vec::new();
11089 if let Some(frame) = self.frames.last() {
11090 for (name, reg) in &frame.prototype.top_level_locals {
11091 let idx = frame.base + *reg as usize;
11092 if let Some(v) = self.stack.get(idx)
11093 && !matches!(v, VmValue::None)
11094 {
11095 out.push((name.clone(), v.clone()));
11096 }
11097 }
11098 }
11099 out
11100 }
11101
11102 fn eval_ast_to_string(&mut self, expr: &AstExpr) -> Result<String, TlError> {
11103 match self.eval_ast_to_vm(expr)? {
11104 VmValue::String(s) => Ok(s.to_string()),
11105 _ => Err(runtime_err("Expected a string")),
11106 }
11107 }
11108
11109 fn interpolate_string(&self, s: &str, _base: usize) -> Result<String, TlError> {
11111 let mut result = String::new();
11112 let mut chars = s.chars().peekable();
11113 while let Some(ch) = chars.next() {
11114 if ch == '{' {
11115 let mut var_name = String::new();
11116 let mut depth = 1;
11117 for c in chars.by_ref() {
11118 if c == '{' {
11119 depth += 1;
11120 } else if c == '}' {
11121 depth -= 1;
11122 if depth == 0 {
11123 break;
11124 }
11125 }
11126 var_name.push(c);
11127 }
11128 if let Some(val) = self.globals.get(&var_name) {
11130 result.push_str(&format!("{val}"));
11131 } else {
11132 result.push('{');
11136 result.push_str(&var_name);
11137 result.push('}');
11138 }
11139 } else if ch == '\\' {
11140 match chars.next() {
11141 Some('n') => result.push('\n'),
11142 Some('t') => result.push('\t'),
11143 Some('\\') => result.push('\\'),
11144 Some('"') => result.push('"'),
11145 Some(c) => {
11146 result.push('\\');
11147 result.push(c);
11148 }
11149 None => result.push('\\'),
11150 }
11151 } else {
11152 result.push(ch);
11153 }
11154 }
11155 Ok(result)
11156 }
11157
11158 pub fn execute_single_instruction(
11161 &mut self,
11162 inst: u32,
11163 proto: &Prototype,
11164 base: usize,
11165 ) -> Result<Option<VmValue>, TlError> {
11166 use crate::opcode::{decode_a, decode_b, decode_bx, decode_c, decode_op};
11167
11168 let proto = Arc::new(proto.clone());
11169 self.frames.push(CallFrame {
11171 prototype: proto.clone(),
11172 ip: 0,
11173 base,
11174 upvalues: Vec::new(),
11175 });
11176 let frame_idx = self.frames.len() - 1;
11177
11178 let op = decode_op(inst);
11179 let a = decode_a(inst);
11180 let _b = decode_b(inst);
11181 let _c = decode_c(inst);
11182 let bx = decode_bx(inst);
11183
11184 let result = match op {
11187 Op::GetGlobal => {
11188 let name = self.get_string_constant(frame_idx, bx)?;
11189 let val = self
11190 .globals
11191 .get(name.as_ref())
11192 .cloned()
11193 .unwrap_or(VmValue::None);
11194 self.stack[base + a as usize] = val;
11195 Ok(None)
11196 }
11197 Op::SetGlobal => {
11198 let name = self.get_string_constant(frame_idx, bx)?;
11199 let val = self.stack[base + a as usize].clone();
11200 self.globals.insert(name.to_string(), val);
11201 Ok(None)
11202 }
11203 _ => {
11204 Ok(None)
11207 }
11208 };
11209
11210 self.frames.pop();
11211 result
11212 }
11213}
11214
11215impl Default for Vm {
11216 fn default() -> Self {
11217 Self::new()
11218 }
11219}
11220
11221#[cfg(test)]
11222mod tests {
11223 use super::*;
11224 use crate::compiler::compile;
11225 use tl_parser::parse;
11226
11227 fn run(source: &str) -> Result<VmValue, TlError> {
11228 let program = parse(source)?;
11229 let proto = compile(&program)?;
11230 let mut vm = Vm::new();
11231 vm.execute(&proto)
11232 }
11233
11234 fn run_output(source: &str) -> Vec<String> {
11235 let program = parse(source).unwrap();
11236 let proto = compile(&program).unwrap();
11237 let mut vm = Vm::new();
11238 vm.execute(&proto).unwrap();
11239 vm.output
11240 }
11241
11242 #[test]
11243 fn test_vm_arithmetic() {
11244 assert!(matches!(run("1 + 2").unwrap(), VmValue::Int(3)));
11245 assert!(matches!(run("10 - 3").unwrap(), VmValue::Int(7)));
11246 assert!(matches!(run("4 * 5").unwrap(), VmValue::Int(20)));
11247 assert!(matches!(run("10 / 3").unwrap(), VmValue::Int(3)));
11248 assert!(matches!(run("10 % 3").unwrap(), VmValue::Int(1)));
11249 assert!(matches!(run("2 ** 10").unwrap(), VmValue::Int(1024)));
11250 let output = run_output("print(1 + 2)");
11251 assert_eq!(output, vec!["3"]);
11252 }
11253
11254 #[test]
11255 fn test_vm_let_and_print() {
11256 let output = run_output("let x = 42\nprint(x)");
11257 assert_eq!(output, vec!["42"]);
11258 }
11259
11260 #[test]
11261 fn test_vm_function() {
11262 let output = run_output("fn double(n) { n * 2 }\nlet result = double(21)\nprint(result)");
11263 assert_eq!(output, vec!["42"]);
11264 }
11265
11266 #[test]
11267 fn test_vm_if_else() {
11268 let output =
11269 run_output("let x = 10\nif x > 5 { print(\"big\") } else { print(\"small\") }");
11270 assert_eq!(output, vec!["big"]);
11271 }
11272
11273 #[test]
11274 fn test_vm_list() {
11275 let output = run_output("let items = [1, 2, 3]\nprint(len(items))");
11276 assert_eq!(output, vec!["3"]);
11277 }
11278
11279 #[test]
11280 fn test_vm_map_builtin() {
11281 let output = run_output(
11282 "let nums = [1, 2, 3]\nlet doubled = map(nums, (x) => x * 2)\nprint(doubled)",
11283 );
11284 assert_eq!(output, vec!["[2, 4, 6]"]);
11285 }
11286
11287 #[test]
11288 fn test_vm_filter_builtin() {
11289 let output = run_output(
11290 "let nums = [1, 2, 3, 4, 5]\nlet evens = filter(nums, (x) => x % 2 == 0)\nprint(evens)",
11291 );
11292 assert_eq!(output, vec!["[2, 4]"]);
11293 }
11294
11295 #[test]
11296 fn test_vm_for_loop() {
11297 let output = run_output("let sum = 0\nfor i in range(5) { sum = sum + i }\nprint(sum)");
11298 assert_eq!(output, vec!["10"]);
11299 }
11300
11301 #[test]
11302 fn test_vm_closure() {
11303 let output = run_output("let double = (x) => x * 2\nprint(double(5))");
11304 assert_eq!(output, vec!["10"]);
11305 }
11306
11307 #[test]
11308 fn test_vm_sum() {
11309 let output = run_output("print(sum([1, 2, 3, 4]))");
11310 assert_eq!(output, vec!["10"]);
11311 }
11312
11313 #[test]
11314 fn test_vm_reduce() {
11315 let output = run_output(
11316 "let product = reduce([1, 2, 3, 4], 1, (acc, x) => acc * x)\nprint(product)",
11317 );
11318 assert_eq!(output, vec!["24"]);
11319 }
11320
11321 #[test]
11322 fn test_vm_pipe() {
11323 let output = run_output("let result = [1, 2, 3] |> map((x) => x + 10)\nprint(result)");
11324 assert_eq!(output, vec!["[11, 12, 13]"]);
11325 }
11326
11327 #[test]
11328 fn test_vm_comparison() {
11329 let output = run_output("print(5 > 3)");
11330 assert_eq!(output, vec!["true"]);
11331 }
11332
11333 #[test]
11334 fn test_vm_precedence() {
11335 let output = run_output("print(2 + 3 * 4)");
11336 assert_eq!(output, vec!["14"]);
11337 }
11338
11339 #[test]
11340 fn test_vm_match() {
11341 let output =
11342 run_output("let x = 2\nprint(match x { 1 => \"one\", 2 => \"two\", _ => \"other\" })");
11343 assert_eq!(output, vec!["two"]);
11344 }
11345
11346 #[test]
11347 fn test_vm_match_wildcard() {
11348 let output = run_output("print(match 99 { 1 => \"one\", _ => \"other\" })");
11349 assert_eq!(output, vec!["other"]);
11350 }
11351
11352 #[test]
11353 fn test_vm_match_binding() {
11354 let output = run_output("print(match 42 { val => val + 1 })");
11355 assert_eq!(output, vec!["43"]);
11356 }
11357
11358 #[test]
11359 fn test_vm_match_guard() {
11360 let output = run_output(
11361 "let x = 5\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11362 );
11363 assert_eq!(output, vec!["pos"]);
11364 }
11365
11366 #[test]
11367 fn test_vm_match_guard_negative() {
11368 let output = run_output(
11369 "let x = -3\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11370 );
11371 assert_eq!(output, vec!["neg"]);
11372 }
11373
11374 #[test]
11375 fn test_vm_match_guard_zero() {
11376 let output = run_output(
11377 "let x = 0\nprint(match x { n if n > 0 => \"pos\", n if n < 0 => \"neg\", _ => \"zero\" })",
11378 );
11379 assert_eq!(output, vec!["zero"]);
11380 }
11381
11382 #[test]
11383 fn test_vm_match_enum_destructure() {
11384 let output = run_output(
11385 r#"
11386enum Shape { Circle(int64), Rect(int64, int64) }
11387let s = Shape::Circle(5)
11388print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11389"#,
11390 );
11391 assert_eq!(output, vec!["5"]);
11392 }
11393
11394 #[test]
11395 fn test_vm_match_enum_destructure_rect() {
11396 let output = run_output(
11397 r#"
11398enum Shape { Circle(int64), Rect(int64, int64) }
11399let s = Shape::Rect(3, 4)
11400print(match s { Shape::Circle(r) => r, Shape::Rect(w, h) => w * h, _ => 0 })
11401"#,
11402 );
11403 assert_eq!(output, vec!["12"]);
11404 }
11405
11406 #[test]
11407 fn test_vm_match_enum_wildcard_field() {
11408 let output = run_output(
11409 r#"
11410enum Pair { Two(int64, int64) }
11411let p = Pair::Two(10, 20)
11412print(match p { Pair::Two(_, y) => y, _ => 0 })
11413"#,
11414 );
11415 assert_eq!(output, vec!["20"]);
11416 }
11417
11418 #[test]
11419 fn test_vm_match_enum_guard() {
11420 let output = run_output(
11421 r#"
11422enum Result { Ok(int64), Err(string) }
11423let r = Result::Ok(150)
11424print(match r { Result::Ok(v) if v > 100 => "big", Result::Ok(v) => "small", Result::Err(e) => e, _ => "unknown" })
11425"#,
11426 );
11427 assert_eq!(output, vec!["big"]);
11428 }
11429
11430 #[test]
11431 fn test_vm_match_or_pattern() {
11432 let output =
11433 run_output("let x = 2\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11434 assert_eq!(output, vec!["small"]);
11435 }
11436
11437 #[test]
11438 fn test_vm_match_or_pattern_no_match() {
11439 let output =
11440 run_output("let x = 10\nprint(match x { 1 or 2 or 3 => \"small\", _ => \"big\" })");
11441 assert_eq!(output, vec!["big"]);
11442 }
11443
11444 #[test]
11445 fn test_vm_match_string() {
11446 let output = run_output(
11447 r#"let s = "hello"
11448print(match s { "hi" => 1, "hello" => 2, _ => 0 })"#,
11449 );
11450 assert_eq!(output, vec!["2"]);
11451 }
11452
11453 #[test]
11454 fn test_vm_match_bool() {
11455 let output = run_output("print(match true { true => \"yes\", false => \"no\" })");
11456 assert_eq!(output, vec!["yes"]);
11457 }
11458
11459 #[test]
11460 fn test_vm_match_none() {
11461 let output = run_output("print(match none { none => \"nothing\", _ => \"something\" })");
11462 assert_eq!(output, vec!["nothing"]);
11463 }
11464
11465 #[test]
11466 fn test_vm_let_destructure_list() {
11467 let output = run_output("let [a, b, c] = [1, 2, 3]\nprint(a)\nprint(b)\nprint(c)");
11468 assert_eq!(output, vec!["1", "2", "3"]);
11469 }
11470
11471 #[test]
11472 fn test_vm_let_destructure_list_rest() {
11473 let output =
11474 run_output("let [head, ...tail] = [1, 2, 3, 4]\nprint(head)\nprint(len(tail))");
11475 assert_eq!(output, vec!["1", "3"]);
11476 }
11477
11478 #[test]
11479 fn test_vm_let_destructure_struct() {
11480 let output = run_output(
11481 r#"
11482struct Point { x: int64, y: int64 }
11483let p = Point { x: 10, y: 20 }
11484let Point { x, y } = p
11485print(x)
11486print(y)
11487"#,
11488 );
11489 assert_eq!(output, vec!["10", "20"]);
11490 }
11491
11492 #[test]
11493 fn test_vm_let_destructure_struct_anon() {
11494 let output = run_output(
11495 r#"
11496struct Point { x: int64, y: int64 }
11497let p = Point { x: 10, y: 20 }
11498let { x, y } = p
11499print(x)
11500print(y)
11501"#,
11502 );
11503 assert_eq!(output, vec!["10", "20"]);
11504 }
11505
11506 #[test]
11507 fn test_vm_match_struct_pattern() {
11508 let output = run_output(
11509 r#"
11510struct Point { x: int64, y: int64 }
11511let p = Point { x: 1, y: 2 }
11512print(match p { Point { x, y } => x + y, _ => 0 })
11513"#,
11514 );
11515 assert_eq!(output, vec!["3"]);
11516 }
11517
11518 #[test]
11519 fn test_vm_match_list_pattern() {
11520 let output = run_output(
11521 r#"
11522let lst = [1, 2, 3]
11523print(match lst { [a, b, c] => a + b + c, _ => 0 })
11524"#,
11525 );
11526 assert_eq!(output, vec!["6"]);
11527 }
11528
11529 #[test]
11530 fn test_vm_match_list_rest_pattern() {
11531 let output = run_output(
11532 r#"
11533let lst = [10, 20, 30, 40]
11534print(match lst { [head, ...rest] => head, _ => 0 })
11535"#,
11536 );
11537 assert_eq!(output, vec!["10"]);
11538 }
11539
11540 #[test]
11541 fn test_vm_match_list_empty() {
11542 let output = run_output(
11543 r#"
11544let lst = []
11545print(match lst { [] => "empty", _ => "nonempty" })
11546"#,
11547 );
11548 assert_eq!(output, vec!["empty"]);
11549 }
11550
11551 #[test]
11552 fn test_vm_match_list_length_mismatch() {
11553 let output = run_output(
11554 r#"
11555let lst = [1, 2, 3]
11556print(match lst { [a, b] => "two", [a, b, c] => "three", _ => "other" })
11557"#,
11558 );
11559 assert_eq!(output, vec!["three"]);
11560 }
11561
11562 #[test]
11563 fn test_vm_match_negative_literal() {
11564 let output =
11565 run_output("print(match -1 { -1 => \"neg one\", 0 => \"zero\", _ => \"other\" })");
11566 assert_eq!(output, vec!["neg one"]);
11567 }
11568
11569 #[test]
11570 fn test_vm_case_with_pattern() {
11571 let output = run_output(
11572 r#"
11573let x = 5
11574let result = case {
11575 x > 10 => "big",
11576 x > 0 => "positive",
11577 _ => "other"
11578}
11579print(result)
11580"#,
11581 );
11582 assert_eq!(output, vec!["positive"]);
11583 }
11584
11585 #[test]
11586 fn test_vm_parallel_map() {
11587 let result = run("map(range(15000), (x) => x * 2)").unwrap();
11589 if let VmValue::List(items) = result {
11590 assert_eq!(items.len(), 15000);
11591 assert!(matches!(items[0], VmValue::Int(0)));
11592 assert!(matches!(items[1], VmValue::Int(2)));
11593 assert!(matches!(items[14999], VmValue::Int(29998)));
11594 } else {
11595 panic!("Expected list, got {:?}", result);
11596 }
11597 }
11598
11599 #[test]
11600 fn test_vm_parallel_filter() {
11601 let result = run("filter(range(20000), (x) => x % 2 == 0)").unwrap();
11602 if let VmValue::List(items) = result {
11603 assert_eq!(items.len(), 10000);
11604 assert!(matches!(items[0], VmValue::Int(0)));
11605 assert!(matches!(items[1], VmValue::Int(2)));
11606 } else {
11607 panic!("Expected list, got {:?}", result);
11608 }
11609 }
11610
11611 #[test]
11612 fn test_vm_parallel_sum() {
11613 let result = run("sum(range(20000))").unwrap();
11614 assert!(matches!(result, VmValue::Int(199990000)));
11616 }
11617
11618 #[test]
11619 fn test_vm_recursive_fib() {
11620 let output = run_output(
11621 "fn fib(n) { if n <= 1 { n } else { fib(n - 1) + fib(n - 2) } }\nprint(fib(10))",
11622 );
11623 assert_eq!(output, vec!["55"]);
11624 }
11625
11626 #[test]
11627 fn test_vm_if_else_expr() {
11628 let output = run_output(
11630 "fn abs(n) { if n < 0 { 0 - n } else { n } }\nprint(abs(-5))\nprint(abs(3))",
11631 );
11632 assert_eq!(output, vec!["5", "3"]);
11633 }
11634
11635 #[test]
11638 fn test_vm_struct_creation() {
11639 let output = run_output(
11640 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.0, y: 2.0 }\nprint(p.x)\nprint(p.y)",
11641 );
11642 assert_eq!(output, vec!["1.0", "2.0"]);
11643 }
11644
11645 #[test]
11646 fn test_vm_struct_nested() {
11647 let output = run_output(
11648 "struct Point { x: float64, y: float64 }\nstruct Line { start: Point, end_pt: Point }\nlet l = Line { start: Point { x: 0.0, y: 0.0 }, end_pt: Point { x: 1.0, y: 1.0 } }\nprint(l.start.x)",
11649 );
11650 assert_eq!(output, vec!["0.0"]);
11651 }
11652
11653 #[test]
11654 fn test_vm_enum_creation() {
11655 let output = run_output("enum Color { Red, Green, Blue }\nlet c = Color::Red\nprint(c)");
11656 assert_eq!(output, vec!["Color::Red"]);
11657 }
11658
11659 #[test]
11660 fn test_vm_enum_with_fields() {
11661 let output = run_output(
11662 "enum Shape { Circle(float64), Rect(float64, float64) }\nlet s = Shape::Circle(5.0)\nprint(s)",
11663 );
11664 assert!(output[0].contains("Circle"));
11665 }
11666
11667 #[test]
11668 fn test_vm_impl_method() {
11669 let output = run_output(
11670 "struct Counter { value: int64 }\nimpl Counter {\n fn get(self) { self.value }\n}\nlet c = Counter { value: 42 }\nprint(c.get())",
11671 );
11672 assert_eq!(output, vec!["42"]);
11673 }
11674
11675 #[test]
11676 fn test_vm_try_catch_throw() {
11677 let output = run_output("try {\n throw \"oops\"\n} catch e {\n print(e)\n}");
11678 assert_eq!(output, vec!["oops"]);
11679 }
11680
11681 #[test]
11682 fn test_vm_string_split() {
11683 let output = run_output("let parts = \"hello world\".split(\" \")\nprint(parts)");
11684 assert_eq!(output, vec!["[hello, world]"]);
11685 }
11686
11687 #[test]
11688 fn test_vm_string_trim() {
11689 let output = run_output("print(\" hello \".trim())");
11690 assert_eq!(output, vec!["hello"]);
11691 }
11692
11693 #[test]
11694 fn test_vm_string_contains() {
11695 let output = run_output("print(\"hello world\".contains(\"world\"))");
11696 assert_eq!(output, vec!["true"]);
11697 }
11698
11699 #[test]
11700 fn test_vm_string_upper_lower() {
11701 let output = run_output("print(\"hello\".to_upper())\nprint(\"HELLO\".to_lower())");
11702 assert_eq!(output, vec!["HELLO", "hello"]);
11703 }
11704
11705 #[test]
11706 fn test_vm_math_sqrt() {
11707 let output = run_output("print(sqrt(16.0))");
11708 assert_eq!(output, vec!["4.0"]);
11709 }
11710
11711 #[test]
11712 fn test_vm_math_floor_ceil() {
11713 let output = run_output("print(floor(3.7))\nprint(ceil(3.2))");
11714 assert_eq!(output, vec!["3.0", "4.0"]);
11715 }
11716
11717 #[test]
11718 fn test_vm_math_trig() {
11719 let output = run_output("print(sin(0.0))\nprint(cos(0.0))");
11720 assert_eq!(output, vec!["0.0", "1.0"]);
11721 }
11722
11723 #[test]
11724 fn test_vm_assert_pass() {
11725 run("assert(true)").unwrap();
11726 run("assert_eq(1 + 1, 2)").unwrap();
11727 }
11728
11729 #[test]
11730 fn test_vm_assert_fail() {
11731 assert!(run("assert(false)").is_err());
11732 assert!(run("assert_eq(1, 2)").is_err());
11733 }
11734
11735 #[test]
11736 fn test_vm_join() {
11737 let output = run_output("print(join(\", \", [\"a\", \"b\", \"c\"]))");
11738 assert_eq!(output, vec!["a, b, c"]);
11739 }
11740
11741 #[test]
11742 fn test_vm_list_method_len() {
11743 let output = run_output("print([1, 2, 3].len())");
11744 assert_eq!(output, vec!["3"]);
11745 }
11746
11747 #[test]
11748 fn test_vm_list_method_map() {
11749 let output = run_output("print([1, 2, 3].map((x) => x * 2))");
11750 assert_eq!(output, vec!["[2, 4, 6]"]);
11751 }
11752
11753 #[test]
11754 fn test_vm_list_method_filter() {
11755 let output = run_output("print([1, 2, 3, 4, 5].filter((x) => x > 3))");
11756 assert_eq!(output, vec!["[4, 5]"]);
11757 }
11758
11759 #[test]
11760 fn test_vm_string_replace() {
11761 let output = run_output("print(\"hello world\".replace(\"world\", \"rust\"))");
11762 assert_eq!(output, vec!["hello rust"]);
11763 }
11764
11765 #[test]
11766 fn test_vm_string_starts_ends() {
11767 let output = run_output(
11768 "print(\"hello\".starts_with(\"hel\"))\nprint(\"hello\".ends_with(\"llo\"))",
11769 );
11770 assert_eq!(output, vec!["true", "true"]);
11771 }
11772
11773 #[test]
11774 fn test_vm_math_log() {
11775 let result = run("log(1.0)").unwrap();
11776 if let VmValue::Float(f) = result {
11777 assert!((f - 0.0).abs() < 1e-10);
11778 } else {
11779 panic!("Expected float");
11780 }
11781 }
11782
11783 #[test]
11784 fn test_vm_pow_builtin() {
11785 let output = run_output("print(pow(2.0, 10.0))");
11786 assert_eq!(output, vec!["1024.0"]);
11787 }
11788
11789 #[test]
11790 fn test_vm_round_builtin() {
11791 let output = run_output("print(round(3.5))");
11792 assert_eq!(output, vec!["4.0"]);
11793 }
11794
11795 #[test]
11796 fn test_vm_try_catch_runtime_error() {
11797 let output = run_output("try {\n let x = 1 / 0\n} catch e {\n print(e)\n}");
11798 assert_eq!(output, vec!["Division by zero"]);
11799 }
11800
11801 #[test]
11802 fn test_vm_struct_field_access() {
11803 let output = run_output(
11804 "struct Point { x: float64, y: float64 }\nlet p = Point { x: 1.5, y: 2.5 }\nprint(p.x)",
11805 );
11806 assert_eq!(output, vec!["1.5"]);
11807 }
11808
11809 #[test]
11810 fn test_vm_enum_match() {
11811 let output = run_output(
11812 "enum Dir { North, South }\nlet d = Dir::North\nmatch d { Dir::North => print(\"north\"), _ => print(\"other\") }",
11813 );
11814 assert!(!output.is_empty());
11816 }
11817
11818 #[test]
11819 fn test_vm_impl_method_with_args() {
11820 let output = run_output(
11821 "struct Rect { w: float64, h: float64 }\nimpl Rect {\n fn area(self) { self.w * self.h }\n}\nlet r = Rect { w: 3.0, h: 4.0 }\nprint(r.area())",
11822 );
11823 assert_eq!(output, vec!["12.0"]);
11824 }
11825
11826 #[test]
11827 fn test_vm_string_len() {
11828 let output = run_output("print(\"hello\".len())");
11829 assert_eq!(output, vec!["5"]);
11830 }
11831
11832 #[test]
11833 fn test_vm_list_reduce() {
11834 let output = run_output(
11835 "let nums = [1, 2, 3, 4]\nlet s = nums.reduce(0, (acc, x) => acc + x)\nprint(s)",
11836 );
11837 assert_eq!(output, vec!["10"]);
11838 }
11839
11840 #[test]
11841 fn test_vm_nested_try_catch() {
11842 let output = run_output(
11843 "try {\n try {\n throw \"inner\"\n } catch e {\n print(e)\n throw \"outer\"\n }\n} catch e2 {\n print(e2)\n}",
11844 );
11845 assert_eq!(output, vec!["inner", "outer"]);
11846 }
11847
11848 #[test]
11849 fn test_vm_math_pow() {
11850 let output = run_output("print(pow(2.0, 10.0))");
11851 assert_eq!(output, vec!["1024.0"]);
11852 }
11853
11854 #[test]
11857 fn test_vm_json_parse() {
11858 let output = run_output(
11859 r#"let m = map_from("a", 1, "b", "hello")
11860let s = json_stringify(m)
11861let m2 = json_parse(s)
11862print(m2["a"])
11863print(m2["b"])"#,
11864 );
11865 assert_eq!(output, vec!["1", "hello"]);
11866 }
11867
11868 #[test]
11869 fn test_vm_json_stringify() {
11870 let output = run_output(
11871 r#"let m = map_from("x", 1, "y", 2)
11872let s = json_stringify(m)
11873print(s)"#,
11874 );
11875 assert_eq!(output, vec![r#"{"x":1,"y":2}"#]);
11876 }
11877
11878 #[test]
11879 fn test_vm_map_from_and_access() {
11880 let output = run_output(
11881 r#"let m = map_from("a", 10, "b", 20)
11882print(m["a"])
11883print(m.b)"#,
11884 );
11885 assert_eq!(output, vec!["10", "20"]);
11886 }
11887
11888 #[test]
11889 fn test_vm_map_methods() {
11890 let output = run_output(
11891 r#"let m = map_from("a", 1, "b", 2)
11892print(m.keys())
11893print(m.values())
11894print(m.contains_key("a"))
11895print(m.len())"#,
11896 );
11897 assert_eq!(output, vec!["[a, b]", "[1, 2]", "true", "2"]);
11898 }
11899
11900 #[test]
11901 fn test_vm_map_set_index() {
11902 let output = run_output(
11903 r#"let m = map_from("a", 1)
11904m["b"] = 2
11905print(m["b"])"#,
11906 );
11907 assert_eq!(output, vec!["2"]);
11908 }
11909
11910 #[test]
11911 fn test_vm_map_iteration() {
11912 let output = run_output(
11913 r#"let m = map_from("x", 10, "y", 20)
11914for kv in m {
11915 print(kv[0])
11916}"#,
11917 );
11918 assert_eq!(output, vec!["x", "y"]);
11919 }
11920
11921 #[test]
11922 fn test_vm_file_read_write() {
11923 let output = run_output(
11924 r#"write_file("/tmp/tl_vm_test.txt", "vm hello")
11925print(read_file("/tmp/tl_vm_test.txt"))
11926print(file_exists("/tmp/tl_vm_test.txt"))"#,
11927 );
11928 assert_eq!(output, vec!["vm hello", "true"]);
11929 }
11930
11931 #[test]
11932 fn test_vm_env_get_set() {
11933 let output = run_output(
11934 r#"env_set("TL_VM_TEST", "abc")
11935print(env_get("TL_VM_TEST"))"#,
11936 );
11937 assert_eq!(output, vec!["abc"]);
11938 }
11939
11940 #[test]
11941 fn test_vm_regex_match() {
11942 let output = run_output(
11943 r#"print(regex_match("\\d+", "abc123"))
11944print(regex_match("^\\d+$", "abc"))"#,
11945 );
11946 assert_eq!(output, vec!["true", "false"]);
11947 }
11948
11949 #[test]
11950 fn test_vm_regex_find() {
11951 let output = run_output(
11952 r#"let m = regex_find("\\d+", "abc123def456")
11953print(len(m))
11954print(m[0])"#,
11955 );
11956 assert_eq!(output, vec!["2", "123"]);
11957 }
11958
11959 #[test]
11960 fn test_vm_regex_replace() {
11961 let output = run_output(r#"print(regex_replace("\\d+", "abc123", "X"))"#);
11962 assert_eq!(output, vec!["abcX"]);
11963 }
11964
11965 #[test]
11966 fn test_vm_now() {
11967 let output = run_output("let t = now()\nprint(type_of(t))");
11969 assert_eq!(output, vec!["datetime"]);
11970 }
11971
11972 #[test]
11973 fn test_vm_date_format() {
11974 let output = run_output(r#"print(date_format(1704067200000, "%Y-%m-%d"))"#);
11975 assert_eq!(output, vec!["2024-01-01"]);
11976 }
11977
11978 #[test]
11979 fn test_vm_date_parse() {
11980 let output = run_output(r#"print(date_parse("2024-01-01 00:00:00", "%Y-%m-%d %H:%M:%S"))"#);
11981 assert_eq!(output, vec!["2024-01-01 00:00:00"]);
11982 }
11983
11984 #[test]
11985 fn test_vm_string_chars() {
11986 let output = run_output(r#"print(len("hello".chars()))"#);
11987 assert_eq!(output, vec!["5"]);
11988 }
11989
11990 #[test]
11991 fn test_vm_string_repeat() {
11992 let output = run_output(r#"print("ab".repeat(3))"#);
11993 assert_eq!(output, vec!["ababab"]);
11994 }
11995
11996 #[test]
11997 fn test_vm_string_index_of() {
11998 let output = run_output(r#"print("hello world".index_of("world"))"#);
11999 assert_eq!(output, vec!["6"]);
12000 }
12001
12002 #[test]
12003 fn test_vm_string_substring() {
12004 let output = run_output(r#"print("hello world".substring(0, 5))"#);
12005 assert_eq!(output, vec!["hello"]);
12006 }
12007
12008 #[test]
12009 fn test_vm_string_pad() {
12010 let output = run_output(
12011 r#"print("42".pad_left(5, "0"))
12012print("hi".pad_right(5, "."))"#,
12013 );
12014 assert_eq!(output, vec!["00042", "hi..."]);
12015 }
12016
12017 #[test]
12018 fn test_vm_list_sort() {
12019 let output = run_output(r#"print([3, 1, 2].sort())"#);
12020 assert_eq!(output, vec!["[1, 2, 3]"]);
12021 }
12022
12023 #[test]
12024 fn test_vm_list_reverse() {
12025 let output = run_output(r#"print([1, 2, 3].reverse())"#);
12026 assert_eq!(output, vec!["[3, 2, 1]"]);
12027 }
12028
12029 #[test]
12030 fn test_vm_list_contains() {
12031 let output = run_output(
12032 r#"print([1, 2, 3].contains(2))
12033print([1, 2, 3].contains(5))"#,
12034 );
12035 assert_eq!(output, vec!["true", "false"]);
12036 }
12037
12038 #[test]
12039 fn test_vm_list_slice() {
12040 let output = run_output(r#"print([1, 2, 3, 4, 5].slice(1, 4))"#);
12041 assert_eq!(output, vec!["[2, 3, 4]"]);
12042 }
12043
12044 #[test]
12045 fn test_vm_zip() {
12046 let output = run_output(
12047 r#"let p = zip([1, 2], ["a", "b"])
12048print(p[0])"#,
12049 );
12050 assert_eq!(output, vec!["[1, a]"]);
12051 }
12052
12053 #[test]
12054 fn test_vm_enumerate() {
12055 let output = run_output(
12056 r#"let e = enumerate(["a", "b", "c"])
12057print(e[1])"#,
12058 );
12059 assert_eq!(output, vec!["[1, b]"]);
12060 }
12061
12062 #[test]
12063 fn test_vm_bool() {
12064 let output = run_output(
12065 r#"print(bool(1))
12066print(bool(0))
12067print(bool(""))"#,
12068 );
12069 assert_eq!(output, vec!["true", "false", "false"]);
12070 }
12071
12072 #[test]
12073 fn test_vm_range_step() {
12074 let output = run_output(r#"print(range(0, 10, 3))"#);
12075 assert_eq!(output, vec!["[0, 3, 6, 9]"]);
12076 }
12077
12078 #[test]
12079 fn test_vm_int_bool() {
12080 let output = run_output(
12081 r#"print(int(true))
12082print(int(false))"#,
12083 );
12084 assert_eq!(output, vec!["1", "0"]);
12085 }
12086
12087 #[test]
12088 fn test_vm_map_len_typeof() {
12089 let output = run_output(
12090 r#"let m = map_from("a", 1)
12091print(len(m))
12092print(type_of(m))"#,
12093 );
12094 assert_eq!(output, vec!["1", "map"]);
12095 }
12096
12097 #[test]
12098 fn test_vm_json_file_roundtrip() {
12099 let output = run_output(
12100 r#"let data = map_from("name", "vm_test", "count", 99)
12101write_file("/tmp/tl_vm_json.json", json_stringify(data))
12102let parsed = json_parse(read_file("/tmp/tl_vm_json.json"))
12103print(parsed["name"])
12104print(parsed["count"])"#,
12105 );
12106 assert_eq!(output, vec!["vm_test", "99"]);
12107 }
12108
12109 #[test]
12112 fn test_vm_spawn_await_basic() {
12113 let output = run_output(
12114 r#"fn worker() { 42 }
12115let t = spawn(worker)
12116let result = await t
12117print(result)"#,
12118 );
12119 assert_eq!(output, vec!["42"]);
12120 }
12121
12122 #[test]
12123 fn test_vm_spawn_closure_with_capture() {
12124 let output = run_output(
12125 r#"let x = 10
12126let f = () => x + 5
12127let t = spawn(f)
12128print(await t)"#,
12129 );
12130 assert_eq!(output, vec!["15"]);
12131 }
12132
12133 #[test]
12134 fn test_vm_sleep() {
12135 let output = run_output(
12136 r#"sleep(10)
12137print("done")"#,
12138 );
12139 assert_eq!(output, vec!["done"]);
12140 }
12141
12142 #[test]
12143 fn test_vm_await_non_task_passthrough() {
12144 let output = run_output(r#"print(await 42)"#);
12145 assert_eq!(output, vec!["42"]);
12146 }
12147
12148 #[test]
12149 fn test_vm_spawn_multiple_await() {
12150 let output = run_output(
12151 r#"fn w1() { 1 }
12152fn w2() { 2 }
12153fn w3() { 3 }
12154let t1 = spawn(w1)
12155let t2 = spawn(w2)
12156let t3 = spawn(w3)
12157let a = await t1
12158let b = await t2
12159let c = await t3
12160print(a + b + c)"#,
12161 );
12162 assert_eq!(output, vec!["6"]);
12163 }
12164
12165 #[test]
12166 fn test_vm_channel_basic() {
12167 let output = run_output(
12168 r#"let ch = channel()
12169send(ch, 42)
12170let val = recv(ch)
12171print(val)"#,
12172 );
12173 assert_eq!(output, vec!["42"]);
12174 }
12175
12176 #[test]
12177 fn test_vm_channel_between_tasks() {
12178 let output = run_output(
12179 r#"let ch = channel()
12180fn producer() { send(ch, 100) }
12181let t = spawn(producer)
12182let val = recv(ch)
12183await t
12184print(val)"#,
12185 );
12186 assert_eq!(output, vec!["100"]);
12187 }
12188
12189 #[test]
12190 fn test_vm_try_recv_empty() {
12191 let output = run_output(
12192 r#"let ch = channel()
12193let val = try_recv(ch)
12194print(val)"#,
12195 );
12196 assert_eq!(output, vec!["none"]);
12197 }
12198
12199 #[test]
12200 fn test_vm_channel_multiple_values() {
12201 let output = run_output(
12202 r#"let ch = channel()
12203send(ch, 1)
12204send(ch, 2)
12205send(ch, 3)
12206print(recv(ch))
12207print(recv(ch))
12208print(recv(ch))"#,
12209 );
12210 assert_eq!(output, vec!["1", "2", "3"]);
12211 }
12212
12213 #[test]
12214 fn test_vm_channel_producer_consumer() {
12215 let output = run_output(
12216 r#"let ch = channel()
12217fn producer() {
12218 send(ch, 10)
12219 send(ch, 20)
12220 send(ch, 30)
12221}
12222let t = spawn(producer)
12223let a = recv(ch)
12224let b = recv(ch)
12225let c = recv(ch)
12226await t
12227print(a + b + c)"#,
12228 );
12229 assert_eq!(output, vec!["60"]);
12230 }
12231
12232 #[test]
12233 fn test_vm_await_all() {
12234 let output = run_output(
12235 r#"fn w1() { 10 }
12236fn w2() { 20 }
12237fn w3() { 30 }
12238let t1 = spawn(w1)
12239let t2 = spawn(w2)
12240let t3 = spawn(w3)
12241let results = await_all([t1, t2, t3])
12242print(sum(results))"#,
12243 );
12244 assert_eq!(output, vec!["60"]);
12245 }
12246
12247 #[test]
12248 fn test_vm_pmap_basic() {
12249 let output = run_output(
12250 r#"let results = pmap([1, 2, 3], (x) => x * 2)
12251print(results)"#,
12252 );
12253 assert_eq!(output, vec!["[2, 4, 6]"]);
12254 }
12255
12256 #[test]
12257 fn test_vm_pmap_order_preserved() {
12258 let output = run_output(
12259 r#"let results = pmap([10, 20, 30], (x) => x + 1)
12260print(results)"#,
12261 );
12262 assert_eq!(output, vec!["[11, 21, 31]"]);
12263 }
12264
12265 #[test]
12266 fn test_vm_timeout_success() {
12267 let output = run_output(
12268 r#"fn worker() { 42 }
12269let t = spawn(worker)
12270let result = timeout(t, 5000)
12271print(result)"#,
12272 );
12273 assert_eq!(output, vec!["42"]);
12274 }
12275
12276 #[test]
12277 fn test_vm_timeout_failure() {
12278 let output = run_output(
12279 r#"fn slow() { sleep(10000) }
12280let t = spawn(slow)
12281let result = "ok"
12282try {
12283 result = timeout(t, 50)
12284} catch e {
12285 result = e
12286}
12287print(result)"#,
12288 );
12289 assert_eq!(output, vec!["Task timed out"]);
12290 }
12291
12292 #[test]
12293 fn test_vm_spawn_error_propagation() {
12294 let output = run_output(
12295 r#"fn bad() { throw "bad thing" }
12296let result = "ok"
12297try {
12298 let t = spawn(bad)
12299 result = await t
12300} catch e {
12301 result = e
12302}
12303print(result)"#,
12304 );
12305 assert_eq!(output, vec!["bad thing"]);
12306 }
12307
12308 #[test]
12309 fn test_vm_spawn_producer_consumer_pipeline() {
12310 let output = run_output(
12311 r#"let ch = channel()
12312fn producer() {
12313 let mut i = 0
12314 while i < 5 {
12315 send(ch, i * 10)
12316 i = i + 1
12317 }
12318}
12319let t = spawn(producer)
12320let mut total = 0
12321let mut count = 0
12322while count < 5 {
12323 total = total + recv(ch)
12324 count = count + 1
12325}
12326await t
12327print(total)"#,
12328 );
12329 assert_eq!(output, vec!["100"]);
12330 }
12331
12332 #[test]
12333 fn test_vm_type_of_task_channel() {
12334 let output = run_output(
12335 r#"fn worker() { 1 }
12336let t = spawn(worker)
12337let ch = channel()
12338print(type_of(t))
12339print(type_of(ch))
12340await t"#,
12341 );
12342 assert_eq!(output, vec!["task", "channel"]);
12343 }
12344
12345 #[test]
12348 fn test_vm_basic_generator() {
12349 let output = run_output(
12350 r#"fn gen() {
12351 yield 1
12352 yield 2
12353 yield 3
12354}
12355let g = gen()
12356print(next(g))
12357print(next(g))
12358print(next(g))
12359print(next(g))"#,
12360 );
12361 assert_eq!(output, vec!["1", "2", "3", "none"]);
12362 }
12363
12364 #[test]
12365 fn test_vm_generator_exhaustion() {
12366 let output = run_output(
12367 r#"fn gen() {
12368 yield 42
12369}
12370let g = gen()
12371print(next(g))
12372print(next(g))
12373print(next(g))"#,
12374 );
12375 assert_eq!(output, vec!["42", "none", "none"]);
12376 }
12377
12378 #[test]
12379 fn test_vm_generator_with_loop() {
12380 let output = run_output(
12381 r#"fn counter() {
12382 let mut i = 0
12383 while i < 3 {
12384 yield i
12385 i = i + 1
12386 }
12387}
12388let g = counter()
12389print(next(g))
12390print(next(g))
12391print(next(g))
12392print(next(g))"#,
12393 );
12394 assert_eq!(output, vec!["0", "1", "2", "none"]);
12395 }
12396
12397 #[test]
12398 fn test_vm_generator_with_args() {
12399 let output = run_output(
12400 r#"fn count_from(start) {
12401 let mut i = start
12402 while i < start + 3 {
12403 yield i
12404 i = i + 1
12405 }
12406}
12407let g = count_from(10)
12408print(next(g))
12409print(next(g))
12410print(next(g))
12411print(next(g))"#,
12412 );
12413 assert_eq!(output, vec!["10", "11", "12", "none"]);
12414 }
12415
12416 #[test]
12417 fn test_vm_generator_yield_none() {
12418 let output = run_output(
12419 r#"fn gen() {
12420 yield
12421 yield 5
12422}
12423let g = gen()
12424print(next(g))
12425print(next(g))
12426print(next(g))"#,
12427 );
12428 assert_eq!(output, vec!["none", "5", "none"]);
12429 }
12430
12431 #[test]
12432 fn test_vm_is_generator() {
12433 let output = run_output(
12434 r#"fn gen() { yield 1 }
12435let g = gen()
12436print(is_generator(g))
12437print(is_generator(42))
12438print(is_generator(none))"#,
12439 );
12440 assert_eq!(output, vec!["true", "false", "false"]);
12441 }
12442
12443 #[test]
12444 fn test_vm_multiple_generators() {
12445 let output = run_output(
12446 r#"fn gen() {
12447 yield 1
12448 yield 2
12449}
12450let g1 = gen()
12451let g2 = gen()
12452print(next(g1))
12453print(next(g2))
12454print(next(g1))
12455print(next(g2))"#,
12456 );
12457 assert_eq!(output, vec!["1", "1", "2", "2"]);
12458 }
12459
12460 #[test]
12461 fn test_vm_for_over_generator() {
12462 let output = run_output(
12463 r#"fn gen() {
12464 yield 10
12465 yield 20
12466 yield 30
12467}
12468for x in gen() {
12469 print(x)
12470}"#,
12471 );
12472 assert_eq!(output, vec!["10", "20", "30"]);
12473 }
12474
12475 #[test]
12476 fn test_vm_iter_builtin() {
12477 let output = run_output(
12478 r#"let g = iter([1, 2, 3])
12479print(next(g))
12480print(next(g))
12481print(next(g))
12482print(next(g))"#,
12483 );
12484 assert_eq!(output, vec!["1", "2", "3", "none"]);
12485 }
12486
12487 #[test]
12488 fn test_vm_take_builtin() {
12489 let output = run_output(
12490 r#"fn naturals() {
12491 let mut n = 0
12492 while true {
12493 yield n
12494 n = n + 1
12495 }
12496}
12497let g = take(naturals(), 5)
12498print(next(g))
12499print(next(g))
12500print(next(g))
12501print(next(g))
12502print(next(g))
12503print(next(g))"#,
12504 );
12505 assert_eq!(output, vec!["0", "1", "2", "3", "4", "none"]);
12506 }
12507
12508 #[test]
12509 fn test_vm_skip_builtin() {
12510 let output = run_output(
12511 r#"let g = skip(iter([10, 20, 30, 40, 50]), 2)
12512print(next(g))
12513print(next(g))
12514print(next(g))
12515print(next(g))"#,
12516 );
12517 assert_eq!(output, vec!["30", "40", "50", "none"]);
12518 }
12519
12520 #[test]
12521 fn test_vm_gen_collect() {
12522 let output = run_output(
12523 r#"fn gen() {
12524 yield 1
12525 yield 2
12526 yield 3
12527}
12528let result = gen_collect(gen())
12529print(result)"#,
12530 );
12531 assert_eq!(output, vec!["[1, 2, 3]"]);
12532 }
12533
12534 #[test]
12535 fn test_vm_gen_map() {
12536 let output = run_output(
12537 r#"let g = gen_map(iter([1, 2, 3]), (x) => x * 10)
12538print(gen_collect(g))"#,
12539 );
12540 assert_eq!(output, vec!["[10, 20, 30]"]);
12541 }
12542
12543 #[test]
12544 fn test_vm_gen_filter() {
12545 let output = run_output(
12546 r#"let g = gen_filter(iter([1, 2, 3, 4, 5, 6]), (x) => x % 2 == 0)
12547print(gen_collect(g))"#,
12548 );
12549 assert_eq!(output, vec!["[2, 4, 6]"]);
12550 }
12551
12552 #[test]
12553 fn test_vm_chain() {
12554 let output = run_output(
12555 r#"let g = chain(iter([1, 2]), iter([3, 4]))
12556print(gen_collect(g))"#,
12557 );
12558 assert_eq!(output, vec!["[1, 2, 3, 4]"]);
12559 }
12560
12561 #[test]
12562 fn test_vm_gen_zip() {
12563 let output = run_output(
12564 r#"let g = gen_zip(iter([1, 2, 3]), iter([10, 20, 30]))
12565print(gen_collect(g))"#,
12566 );
12567 assert_eq!(output, vec!["[[1, 10], [2, 20], [3, 30]]"]);
12568 }
12569
12570 #[test]
12571 fn test_vm_gen_enumerate() {
12572 let output = run_output(
12573 r#"let g = gen_enumerate(iter([10, 20, 30]))
12574print(gen_collect(g))"#,
12575 );
12576 assert_eq!(output, vec!["[[0, 10], [1, 20], [2, 30]]"]);
12577 }
12578
12579 #[test]
12580 fn test_vm_combinator_chaining() {
12581 let output = run_output(
12582 r#"fn naturals() {
12583 let mut n = 0
12584 while true {
12585 yield n
12586 n = n + 1
12587 }
12588}
12589let result = gen_collect(gen_map(gen_filter(take(naturals(), 10), (x) => x % 2 == 0), (x) => x * x))
12590print(result)"#,
12591 );
12592 assert_eq!(output, vec!["[0, 4, 16, 36, 64]"]);
12593 }
12594
12595 #[test]
12596 fn test_vm_for_over_take() {
12597 let output = run_output(
12598 r#"fn naturals() {
12599 let mut n = 0
12600 while true {
12601 yield n
12602 n = n + 1
12603 }
12604}
12605for x in take(naturals(), 5) {
12606 print(x)
12607}"#,
12608 );
12609 assert_eq!(output, vec!["0", "1", "2", "3", "4"]);
12610 }
12611
12612 #[test]
12613 fn test_vm_generator_error_propagation() {
12614 let result = run(r#"fn bad_gen() {
12615 yield 1
12616 throw "oops"
12617}
12618let g = bad_gen()
12619let mut caught = ""
12620next(g)
12621try {
12622 next(g)
12623} catch e {
12624 caught = e
12625}
12626print(caught)"#);
12627 assert!(result.is_ok());
12629 }
12630
12631 #[test]
12632 fn test_vm_fibonacci_generator() {
12633 let output = run_output(
12634 r#"fn fib() {
12635 let mut a = 0
12636 let mut b = 1
12637 while true {
12638 yield a
12639 let temp = a + b
12640 a = b
12641 b = temp
12642 }
12643}
12644print(gen_collect(take(fib(), 8)))"#,
12645 );
12646 assert_eq!(output, vec!["[0, 1, 1, 2, 3, 5, 8, 13]"]);
12647 }
12648
12649 #[test]
12650 fn test_vm_generator_method_syntax() {
12651 let output = run_output(
12652 r#"fn gen() {
12653 yield 1
12654 yield 2
12655 yield 3
12656}
12657let g = gen()
12658print(type_of(g))"#,
12659 );
12660 assert_eq!(output, vec!["generator"]);
12661 }
12662
12663 #[test]
12666 fn test_vm_ok_err_builtins() {
12667 let output = run_output("let r = Ok(42)\nprint(r)");
12668 assert_eq!(output, vec!["Result::Ok(42)"]);
12669
12670 let output = run_output("let r = Err(\"fail\")\nprint(r)");
12671 assert_eq!(output, vec!["Result::Err(fail)"]);
12672 }
12673
12674 #[test]
12675 fn test_vm_is_ok_is_err() {
12676 let output = run_output("print(is_ok(Ok(42)))");
12677 assert_eq!(output, vec!["true"]);
12678 let output = run_output("print(is_err(Ok(42)))");
12679 assert_eq!(output, vec!["false"]);
12680 let output = run_output("print(is_ok(Err(\"fail\")))");
12681 assert_eq!(output, vec!["false"]);
12682 let output = run_output("print(is_err(Err(\"fail\")))");
12683 assert_eq!(output, vec!["true"]);
12684 }
12685
12686 #[test]
12687 fn test_vm_unwrap_ok() {
12688 let output = run_output("print(unwrap(Ok(42)))");
12689 assert_eq!(output, vec!["42"]);
12690 }
12691
12692 #[test]
12693 fn test_vm_unwrap_err_panics() {
12694 let result = run("unwrap(Err(\"fail\"))");
12695 assert!(result.is_err());
12696 }
12697
12698 #[test]
12699 fn test_vm_try_on_ok() {
12700 let output = run_output(
12701 r#"fn get_val() { Ok(42) }
12702fn process() { let v = get_val()? + 1
12703Ok(v) }
12704print(process())"#,
12705 );
12706 assert_eq!(output, vec!["Result::Ok(43)"]);
12707 }
12708
12709 #[test]
12710 fn test_vm_try_on_err_propagates() {
12711 let output = run_output(
12712 r#"fn failing() { Err("oops") }
12713fn process() { let v = failing()?
12714Ok(v) }
12715print(process())"#,
12716 );
12717 assert_eq!(output, vec!["Result::Err(oops)"]);
12718 }
12719
12720 #[test]
12721 fn test_vm_try_on_none_propagates() {
12722 let output = run_output(
12723 r#"fn get_none() { none }
12724fn process() { let v = get_none()?
1272542 }
12726print(process())"#,
12727 );
12728 assert_eq!(output, vec!["none"]);
12729 }
12730
12731 #[test]
12732 fn test_vm_try_passthrough() {
12733 let output = run_output(
12735 r#"fn get_val() { 42 }
12736fn process() { let v = get_val()?
12737v + 1 }
12738print(process())"#,
12739 );
12740 assert_eq!(output, vec!["43"]);
12741 }
12742
12743 #[test]
12744 fn test_vm_result_match() {
12745 let output = run_output(
12746 r#"let r = Ok(42)
12747print(is_ok(r))
12748print(unwrap(r))"#,
12749 );
12750 assert_eq!(output, vec!["true", "42"]);
12751 }
12752
12753 #[test]
12754 fn test_vm_result_match_err() {
12755 let output = run_output(
12756 r#"let r = Err("fail")
12757print(is_err(r))
12758match r {
12759 Result::Err(e) => print("got error"),
12760 _ => print("no error")
12761}"#,
12762 );
12763 assert_eq!(output, vec!["true", "got error"]);
12764 }
12765
12766 #[test]
12769 fn test_vm_set_from_dedup() {
12770 let output = run_output(
12771 r#"let s = set_from([1, 2, 3, 2, 1])
12772print(len(s))
12773print(type_of(s))"#,
12774 );
12775 assert_eq!(output, vec!["3", "set"]);
12776 }
12777
12778 #[test]
12779 fn test_vm_set_add() {
12780 let output = run_output(
12781 r#"let s = set_from([1, 2])
12782let s2 = set_add(s, 3)
12783let s3 = set_add(s2, 2)
12784print(len(s2))
12785print(len(s3))"#,
12786 );
12787 assert_eq!(output, vec!["3", "3"]);
12788 }
12789
12790 #[test]
12791 fn test_vm_set_remove() {
12792 let output = run_output(
12793 r#"let s = set_from([1, 2, 3])
12794let s2 = set_remove(s, 2)
12795print(len(s2))
12796print(set_contains(s2, 2))"#,
12797 );
12798 assert_eq!(output, vec!["2", "false"]);
12799 }
12800
12801 #[test]
12802 fn test_vm_set_contains() {
12803 let output = run_output(
12804 r#"let s = set_from([1, 2, 3])
12805print(set_contains(s, 2))
12806print(set_contains(s, 5))"#,
12807 );
12808 assert_eq!(output, vec!["true", "false"]);
12809 }
12810
12811 #[test]
12812 fn test_vm_set_union() {
12813 let output = run_output(
12814 r#"let a = set_from([1, 2, 3])
12815let b = set_from([3, 4, 5])
12816let c = set_union(a, b)
12817print(len(c))"#,
12818 );
12819 assert_eq!(output, vec!["5"]);
12820 }
12821
12822 #[test]
12823 fn test_vm_set_intersection() {
12824 let output = run_output(
12825 r#"let a = set_from([1, 2, 3])
12826let b = set_from([2, 3, 4])
12827let c = set_intersection(a, b)
12828print(len(c))"#,
12829 );
12830 assert_eq!(output, vec!["2"]);
12831 }
12832
12833 #[test]
12834 fn test_vm_set_difference() {
12835 let output = run_output(
12836 r#"let a = set_from([1, 2, 3])
12837let b = set_from([2, 3, 4])
12838let c = set_difference(a, b)
12839print(len(c))"#,
12840 );
12841 assert_eq!(output, vec!["1"]);
12842 }
12843
12844 #[test]
12845 fn test_vm_set_for_loop() {
12846 let output = run_output(
12847 r#"let s = set_from([10, 20, 30])
12848let total = 0
12849for item in s {
12850 total = total + item
12851}
12852print(total)"#,
12853 );
12854 assert_eq!(output, vec!["60"]);
12855 }
12856
12857 #[test]
12858 fn test_vm_set_to_list() {
12859 let output = run_output(
12860 r#"let s = set_from([3, 1, 2])
12861let lst = s.to_list()
12862print(type_of(lst))
12863print(len(lst))"#,
12864 );
12865 assert_eq!(output, vec!["list", "3"]);
12866 }
12867
12868 #[test]
12869 fn test_vm_set_method_contains() {
12870 let output = run_output(
12871 r#"let s = set_from([1, 2, 3])
12872print(s.contains(2))
12873print(s.contains(5))"#,
12874 );
12875 assert_eq!(output, vec!["true", "false"]);
12876 }
12877
12878 #[test]
12879 fn test_vm_set_method_add_remove() {
12880 let output = run_output(
12881 r#"let s = set_from([1, 2])
12882let s2 = s.add(3)
12883print(s2.len())
12884let s3 = s2.remove(1)
12885print(s3.len())"#,
12886 );
12887 assert_eq!(output, vec!["3", "2"]);
12888 }
12889
12890 #[test]
12891 fn test_vm_set_method_union_intersection_difference() {
12892 let output = run_output(
12893 r#"let a = set_from([1, 2, 3])
12894let b = set_from([2, 3, 4])
12895print(a.union(b).len())
12896print(a.intersection(b).len())
12897print(a.difference(b).len())"#,
12898 );
12899 assert_eq!(output, vec!["4", "2", "1"]);
12900 }
12901
12902 #[test]
12903 fn test_vm_set_empty() {
12904 let output = run_output(
12905 r#"let s = set_from([])
12906print(len(s))
12907let s2 = s.add(1)
12908print(len(s2))"#,
12909 );
12910 assert_eq!(output, vec!["0", "1"]);
12911 }
12912
12913 #[test]
12914 fn test_vm_set_string_values() {
12915 let output = run_output(
12916 r#"let s = set_from(["a", "b", "a", "c"])
12917print(len(s))
12918print(s.contains("b"))"#,
12919 );
12920 assert_eq!(output, vec!["3", "true"]);
12921 }
12922
12923 #[test]
12926 fn test_vm_import_with_caching() {
12927 let vm = Vm::new();
12929 assert!(vm.module_cache.is_empty());
12930 assert!(vm.importing_files.is_empty());
12931 assert!(vm.file_path.is_none());
12932 }
12933
12934 #[test]
12935 fn test_vm_use_single_file() {
12936 let dir = tempfile::tempdir().unwrap();
12938 let lib_path = dir.path().join("math.tl");
12939 std::fs::write(&lib_path, "let PI = 3.14\nfn add(a, b) { a + b }").unwrap();
12940
12941 let main_path = dir.path().join("main.tl");
12942 std::fs::write(&main_path, "use math\nprint(add(1, 2))").unwrap();
12943
12944 let source = std::fs::read_to_string(&main_path).unwrap();
12945 let program = tl_parser::parse(&source).unwrap();
12946 let proto = crate::compiler::compile(&program).unwrap();
12947
12948 let mut vm = Vm::new();
12949 vm.file_path = Some(main_path.to_string_lossy().to_string());
12950 vm.execute(&proto).unwrap();
12951 assert_eq!(vm.output, vec!["3"]);
12952 }
12953
12954 #[test]
12955 fn test_vm_use_wildcard() {
12956 let dir = tempfile::tempdir().unwrap();
12957 std::fs::write(
12958 dir.path().join("helpers.tl"),
12959 "fn greet() { \"hello\" }\nfn farewell() { \"bye\" }",
12960 )
12961 .unwrap();
12962
12963 let main_src = "use helpers.*\nprint(greet())\nprint(farewell())";
12964 let main_path = dir.path().join("main.tl");
12965 std::fs::write(&main_path, main_src).unwrap();
12966
12967 let program = tl_parser::parse(main_src).unwrap();
12968 let proto = crate::compiler::compile(&program).unwrap();
12969
12970 let mut vm = Vm::new();
12971 vm.file_path = Some(main_path.to_string_lossy().to_string());
12972 vm.execute(&proto).unwrap();
12973 assert_eq!(vm.output, vec!["hello", "bye"]);
12974 }
12975
12976 #[test]
12977 fn test_vm_use_aliased() {
12978 let dir = tempfile::tempdir().unwrap();
12979 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
12980
12981 let main_src = "use mylib as m\nprint(m.compute())";
12982 let main_path = dir.path().join("main.tl");
12983 std::fs::write(&main_path, main_src).unwrap();
12984
12985 let program = tl_parser::parse(main_src).unwrap();
12986 let proto = crate::compiler::compile(&program).unwrap();
12987
12988 let mut vm = Vm::new();
12989 vm.file_path = Some(main_path.to_string_lossy().to_string());
12990 vm.execute(&proto).unwrap();
12991 assert_eq!(vm.output, vec!["42"]);
12992 }
12993
12994 #[test]
12995 fn test_vm_use_directory_module() {
12996 let dir = tempfile::tempdir().unwrap();
12997 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
12998 std::fs::write(dir.path().join("utils/mod.tl"), "fn helper() { 99 }").unwrap();
12999
13000 let main_src = "use utils\nprint(helper())";
13001 let main_path = dir.path().join("main.tl");
13002 std::fs::write(&main_path, main_src).unwrap();
13003
13004 let program = tl_parser::parse(main_src).unwrap();
13005 let proto = crate::compiler::compile(&program).unwrap();
13006
13007 let mut vm = Vm::new();
13008 vm.file_path = Some(main_path.to_string_lossy().to_string());
13009 vm.execute(&proto).unwrap();
13010 assert_eq!(vm.output, vec!["99"]);
13011 }
13012
13013 #[test]
13014 fn test_vm_circular_import_detection() {
13015 let dir = tempfile::tempdir().unwrap();
13016 let a_path = dir.path().join("a.tl");
13017 let b_path = dir.path().join("b.tl");
13018 std::fs::write(&a_path, &format!("import \"{}\"", b_path.to_string_lossy())).unwrap();
13019 std::fs::write(&b_path, &format!("import \"{}\"", a_path.to_string_lossy())).unwrap();
13020
13021 let source = std::fs::read_to_string(&a_path).unwrap();
13022 let program = tl_parser::parse(&source).unwrap();
13023 let proto = crate::compiler::compile(&program).unwrap();
13024
13025 let mut vm = Vm::new();
13026 vm.file_path = Some(a_path.to_string_lossy().to_string());
13027 let result = vm.execute(&proto);
13028 assert!(result.is_err());
13029 assert!(format!("{:?}", result).contains("Circular import"));
13030 }
13031
13032 #[test]
13033 fn test_vm_module_caching() {
13034 let dir = tempfile::tempdir().unwrap();
13036 std::fs::write(dir.path().join("cached.tl"), "let X = 42").unwrap();
13037
13038 let main_src = "use cached\nuse cached\nprint(X)";
13039 let main_path = dir.path().join("main.tl");
13040 std::fs::write(&main_path, main_src).unwrap();
13041
13042 let program = tl_parser::parse(main_src).unwrap();
13043 let proto = crate::compiler::compile(&program).unwrap();
13044
13045 let mut vm = Vm::new();
13046 vm.file_path = Some(main_path.to_string_lossy().to_string());
13047 vm.execute(&proto).unwrap();
13048 assert_eq!(vm.output, vec!["42"]);
13049 }
13050
13051 #[test]
13052 fn test_vm_existing_import_still_works() {
13053 let dir = tempfile::tempdir().unwrap();
13055 let lib_path = dir.path().join("lib.tl");
13056 std::fs::write(&lib_path, "fn imported_fn() { 123 }").unwrap();
13057
13058 let main_src = format!(
13059 "import \"{}\"\nprint(imported_fn())",
13060 lib_path.to_string_lossy()
13061 );
13062 let program = tl_parser::parse(&main_src).unwrap();
13063 let proto = crate::compiler::compile(&program).unwrap();
13064
13065 let mut vm = Vm::new();
13066 vm.execute(&proto).unwrap();
13067 assert_eq!(vm.output, vec!["123"]);
13068 }
13069
13070 #[test]
13071 fn test_vm_pub_fn_parsing() {
13072 let output = run_output("pub fn add(a, b) { a + b }\nprint(add(1, 2))");
13074 assert_eq!(output, vec!["3"]);
13075 }
13076
13077 #[test]
13078 fn test_vm_use_nested_path() {
13079 let dir = tempfile::tempdir().unwrap();
13080 std::fs::create_dir_all(dir.path().join("data")).unwrap();
13081 std::fs::write(
13082 dir.path().join("data/transforms.tl"),
13083 "fn clean(x) { x + 1 }",
13084 )
13085 .unwrap();
13086
13087 let main_src = "use data.transforms\nprint(clean(41))";
13088 let main_path = dir.path().join("main.tl");
13089 std::fs::write(&main_path, main_src).unwrap();
13090
13091 let program = tl_parser::parse(main_src).unwrap();
13092 let proto = crate::compiler::compile(&program).unwrap();
13093
13094 let mut vm = Vm::new();
13095 vm.file_path = Some(main_path.to_string_lossy().to_string());
13096 vm.execute(&proto).unwrap();
13097 assert_eq!(vm.output, vec!["42"]);
13098 }
13099
13100 #[test]
13103 fn test_integration_multi_file_use_functions() {
13104 let dir = tempfile::tempdir().unwrap();
13106 std::fs::write(
13107 dir.path().join("lib.tl"),
13108 "fn greet(name) { \"Hello, \" + name + \"!\" }\nfn double(x) { x * 2 }",
13109 )
13110 .unwrap();
13111
13112 let main_src = "use lib\nprint(greet(\"World\"))\nprint(double(21))";
13113 let main_path = dir.path().join("main.tl");
13114 std::fs::write(&main_path, main_src).unwrap();
13115
13116 let program = tl_parser::parse(main_src).unwrap();
13117 let proto = crate::compiler::compile(&program).unwrap();
13118 let mut vm = Vm::new();
13119 vm.file_path = Some(main_path.to_string_lossy().to_string());
13120 vm.execute(&proto).unwrap();
13121 assert_eq!(vm.output, vec!["Hello, World!", "42"]);
13122 }
13123
13124 #[test]
13125 fn test_integration_mixed_import_and_use() {
13126 let dir = tempfile::tempdir().unwrap();
13128 std::fs::write(dir.path().join("old_lib.tl"), "fn old_fn() { 10 }").unwrap();
13129 std::fs::write(dir.path().join("new_lib.tl"), "fn new_fn() { 20 }").unwrap();
13130
13131 let old_lib_abs = dir.path().join("old_lib.tl").to_string_lossy().to_string();
13132 let main_src = format!("import \"{old_lib_abs}\"\nuse new_lib\nprint(old_fn() + new_fn())");
13133 let main_path = dir.path().join("main.tl");
13134 std::fs::write(&main_path, &main_src).unwrap();
13135
13136 let program = tl_parser::parse(&main_src).unwrap();
13137 let proto = crate::compiler::compile(&program).unwrap();
13138 let mut vm = Vm::new();
13139 vm.file_path = Some(main_path.to_string_lossy().to_string());
13140 vm.execute(&proto).unwrap();
13141 assert_eq!(vm.output, vec!["30"]);
13142 }
13143
13144 #[test]
13145 fn test_integration_directory_module_with_mod_tl() {
13146 let dir = tempfile::tempdir().unwrap();
13148 std::fs::create_dir_all(dir.path().join("utils")).unwrap();
13149 std::fs::write(
13150 dir.path().join("utils/mod.tl"),
13151 "fn helper() { 99 }\nfn format_num(n) { str(n) + \"!\" }",
13152 )
13153 .unwrap();
13154
13155 let main_src = "use utils\nprint(helper())\nprint(format_num(42))";
13156 let main_path = dir.path().join("main.tl");
13157 std::fs::write(&main_path, main_src).unwrap();
13158
13159 let program = tl_parser::parse(main_src).unwrap();
13160 let proto = crate::compiler::compile(&program).unwrap();
13161 let mut vm = Vm::new();
13162 vm.file_path = Some(main_path.to_string_lossy().to_string());
13163 vm.execute(&proto).unwrap();
13164 assert_eq!(vm.output, vec!["99", "42!"]);
13165 }
13166
13167 #[test]
13168 fn test_integration_circular_dep_error() {
13169 let dir = tempfile::tempdir().unwrap();
13170 let a_abs = dir.path().join("a.tl").to_string_lossy().to_string();
13171 let b_abs = dir.path().join("b.tl").to_string_lossy().to_string();
13172 std::fs::write(
13173 dir.path().join("a.tl"),
13174 format!("import \"{b_abs}\"\nfn fa() {{ 1 }}"),
13175 )
13176 .unwrap();
13177 std::fs::write(
13178 dir.path().join("b.tl"),
13179 format!("import \"{a_abs}\"\nfn fb() {{ 2 }}"),
13180 )
13181 .unwrap();
13182
13183 let main_src = format!("import \"{a_abs}\"");
13184 let program = tl_parser::parse(&main_src).unwrap();
13185 let proto = crate::compiler::compile(&program).unwrap();
13186 let mut vm = Vm::new();
13187 let result = vm.execute(&proto);
13188 assert!(result.is_err());
13189 let err_msg = format!("{}", result.unwrap_err());
13190 assert!(
13191 err_msg.contains("Circular") || err_msg.contains("circular"),
13192 "Expected circular import error, got: {err_msg}"
13193 );
13194 }
13195
13196 #[test]
13197 fn test_integration_use_aliased_method_call() {
13198 let dir = tempfile::tempdir().unwrap();
13200 std::fs::write(dir.path().join("mylib.tl"), "fn compute() { 42 }").unwrap();
13201
13202 let main_src = "use mylib as m\nprint(m.compute())";
13203 let main_path = dir.path().join("main.tl");
13204 std::fs::write(&main_path, main_src).unwrap();
13205
13206 let program = tl_parser::parse(main_src).unwrap();
13207 let proto = crate::compiler::compile(&program).unwrap();
13208 let mut vm = Vm::new();
13209 vm.file_path = Some(main_path.to_string_lossy().to_string());
13210 vm.execute(&proto).unwrap();
13211 assert_eq!(vm.output, vec!["42"]);
13212 }
13213
13214 #[test]
13215 fn test_integration_module_caching_shared() {
13216 let dir = tempfile::tempdir().unwrap();
13218 std::fs::write(dir.path().join("shared.tl"), "fn get_val() { 42 }").unwrap();
13219
13220 let main_src = "use shared\nprint(get_val())\nuse shared\nprint(get_val())";
13221 let main_path = dir.path().join("main.tl");
13222 std::fs::write(&main_path, main_src).unwrap();
13223
13224 let program = tl_parser::parse(main_src).unwrap();
13225 let proto = crate::compiler::compile(&program).unwrap();
13226 let mut vm = Vm::new();
13227 vm.file_path = Some(main_path.to_string_lossy().to_string());
13228 vm.execute(&proto).unwrap();
13229 assert_eq!(vm.output, vec!["42", "42"]);
13230 }
13231
13232 #[test]
13233 fn test_integration_pub_keyword_in_module() {
13234 let dir = tempfile::tempdir().unwrap();
13236 std::fs::write(
13237 dir.path().join("pubmod.tl"),
13238 "pub fn public_fn() { 100 }\nfn private_fn() { 200 }",
13239 )
13240 .unwrap();
13241
13242 let main_src = "use pubmod\nprint(public_fn())";
13243 let main_path = dir.path().join("main.tl");
13244 std::fs::write(&main_path, main_src).unwrap();
13245
13246 let program = tl_parser::parse(main_src).unwrap();
13247 let proto = crate::compiler::compile(&program).unwrap();
13248 let mut vm = Vm::new();
13249 vm.file_path = Some(main_path.to_string_lossy().to_string());
13250 vm.execute(&proto).unwrap();
13251 assert_eq!(vm.output, vec!["100"]);
13252 }
13253
13254 #[test]
13255 fn test_integration_backward_compat_import_as() {
13256 let dir = tempfile::tempdir().unwrap();
13258 let lib_path = dir.path().join("mylib.tl");
13259 std::fs::write(&lib_path, "fn compute() { 77 }").unwrap();
13260
13261 let main_src = format!(
13262 "import \"{}\" as m\nprint(m.compute())",
13263 lib_path.to_string_lossy()
13264 );
13265 let program = tl_parser::parse(&main_src).unwrap();
13266 let proto = crate::compiler::compile(&program).unwrap();
13267 let mut vm = Vm::new();
13268 vm.execute(&proto).unwrap();
13269 assert_eq!(vm.output, vec!["77"]);
13270 }
13271
13272 #[test]
13275 fn test_vm_generic_fn() {
13276 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(42))");
13277 assert_eq!(output, vec!["42"]);
13278 }
13279
13280 #[test]
13281 fn test_vm_generic_fn_string() {
13282 let output = run_output("fn identity<T>(x: T) -> T { x }\nprint(identity(\"hello\"))");
13283 assert_eq!(output, vec!["hello"]);
13284 }
13285
13286 #[test]
13287 fn test_vm_generic_struct() {
13288 let output = run_output(
13289 "struct Pair<A, B> { first: A, second: B }\nlet p = Pair { first: 1, second: \"hi\" }\nprint(p.first)\nprint(p.second)",
13290 );
13291 assert_eq!(output, vec!["1", "hi"]);
13292 }
13293
13294 #[test]
13295 fn test_vm_trait_def_noop() {
13296 let output = run_output("trait Display { fn show(self) -> string }\nprint(\"ok\")");
13298 assert_eq!(output, vec!["ok"]);
13299 }
13300
13301 #[test]
13302 fn test_vm_trait_impl_methods() {
13303 let output = run_output(
13304 "struct Point { x: int, y: int }\nimpl Display for Point { fn show(self) -> string { \"point\" } }\nlet p = Point { x: 1, y: 2 }\nprint(p.show())",
13305 );
13306 assert_eq!(output, vec!["point"]);
13307 }
13308
13309 #[test]
13310 fn test_vm_generic_enum() {
13311 let output = run_output(
13313 "enum MyOpt<T> { Some(T), Nothing }\nlet x = MyOpt::Some(42)\nprint(type_of(x))",
13314 );
13315 assert_eq!(output, vec!["enum"]);
13316 }
13317
13318 #[test]
13319 fn test_vm_where_clause_runtime() {
13320 let output =
13322 run_output("fn compare<T>(x: T) where T: Comparable { x }\nprint(compare(10))");
13323 assert_eq!(output, vec!["10"]);
13324 }
13325
13326 #[test]
13327 fn test_vm_trait_impl_self_method() {
13328 let output = run_output(
13329 "struct Counter { value: int }\nimpl Incrementable for Counter { fn inc(self) { self.value + 1 } }\nlet c = Counter { value: 5 }\nprint(c.inc())",
13330 );
13331 assert_eq!(output, vec!["6"]);
13332 }
13333
13334 #[test]
13337 fn test_vm_generic_fn_with_type_inference() {
13338 let output = run_output(
13340 "fn first<T>(xs: list<T>) -> T { xs[0] }\nprint(first([1, 2, 3]))\nprint(first([\"a\", \"b\"]))",
13341 );
13342 assert_eq!(output, vec!["1", "a"]);
13343 }
13344
13345 #[test]
13346 fn test_vm_generic_struct_with_methods() {
13347 let output = run_output(
13348 "struct Box<T> { val: T }\nimpl Box { fn get(self) { self.val } }\nlet b = Box { val: 42 }\nprint(b.get())",
13349 );
13350 assert_eq!(output, vec!["42"]);
13351 }
13352
13353 #[test]
13354 fn test_vm_trait_def_impl_call() {
13355 let output = run_output(
13356 "trait Greetable { fn greet(self) -> string }\nstruct Person { name: string }\nimpl Greetable for Person { fn greet(self) -> string { self.name } }\nlet p = Person { name: \"Alice\" }\nprint(p.greet())",
13357 );
13358 assert_eq!(output, vec!["Alice"]);
13359 }
13360
13361 #[test]
13362 fn test_vm_multiple_generic_params() {
13363 let output = run_output(
13364 "fn pair<A, B>(a: A, b: B) { [a, b] }\nlet p = pair(1, \"two\")\nprint(len(p))",
13365 );
13366 assert_eq!(output, vec!["2"]);
13367 }
13368
13369 #[test]
13370 fn test_vm_backward_compat_non_generic() {
13371 let output = run_output(
13373 "fn add(a, b) { a + b }\nstruct Point { x: int, y: int }\nimpl Point { fn sum(self) { self.x + self.y } }\nlet p = Point { x: 3, y: 4 }\nprint(add(1, 2))\nprint(p.sum())",
13374 );
13375 assert_eq!(output, vec!["3", "7"]);
13376 }
13377
13378 #[test]
13381 fn test_vm_package_import_resolves() {
13382 let tmp = tempfile::tempdir().unwrap();
13384 let pkg_dir = tmp.path().join("mylib");
13385 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13386 std::fs::write(
13387 pkg_dir.join("src/lib.tl"),
13388 "pub fn greet() { print(\"hello from pkg\") }",
13389 )
13390 .unwrap();
13391 std::fs::write(
13392 pkg_dir.join("tl.toml"),
13393 "[project]\nname = \"mylib\"\nversion = \"1.0.0\"\n",
13394 )
13395 .unwrap();
13396
13397 let main_file = tmp.path().join("main.tl");
13399 std::fs::write(&main_file, "use mylib\ngreet()").unwrap();
13400
13401 let source = std::fs::read_to_string(&main_file).unwrap();
13402 let program = tl_parser::parse(&source).unwrap();
13403 let proto = crate::compiler::compile(&program).unwrap();
13404
13405 let mut vm = Vm::new();
13406 vm.file_path = Some(main_file.to_string_lossy().to_string());
13407 vm.package_roots.insert("mylib".into(), pkg_dir);
13408 vm.execute(&proto).unwrap();
13409
13410 assert_eq!(vm.output, vec!["hello from pkg"]);
13411 }
13412
13413 #[test]
13414 fn test_vm_package_nested_import() {
13415 let tmp = tempfile::tempdir().unwrap();
13416 let pkg_dir = tmp.path().join("utils");
13417 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13418 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13419 std::fs::write(
13420 pkg_dir.join("tl.toml"),
13421 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13422 )
13423 .unwrap();
13424
13425 let main_file = tmp.path().join("main.tl");
13427 std::fs::write(&main_file, "use utils.math\nprint(double(21))").unwrap();
13428
13429 let source = std::fs::read_to_string(&main_file).unwrap();
13430 let program = tl_parser::parse(&source).unwrap();
13431 let proto = crate::compiler::compile(&program).unwrap();
13432
13433 let mut vm = Vm::new();
13434 vm.file_path = Some(main_file.to_string_lossy().to_string());
13435 vm.package_roots.insert("utils".into(), pkg_dir);
13436 vm.execute(&proto).unwrap();
13437
13438 assert_eq!(vm.output, vec!["42"]);
13439 }
13440
13441 #[test]
13442 fn test_vm_package_aliased_import() {
13443 let tmp = tempfile::tempdir().unwrap();
13444 let pkg_dir = tmp.path().join("utils");
13445 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13446 std::fs::write(pkg_dir.join("src/math.tl"), "pub fn double(x) { x * 2 }").unwrap();
13447 std::fs::write(
13448 pkg_dir.join("tl.toml"),
13449 "[project]\nname = \"utils\"\nversion = \"1.0.0\"\n",
13450 )
13451 .unwrap();
13452
13453 let main_file = tmp.path().join("main.tl");
13455 std::fs::write(&main_file, "use utils.math as m\nprint(m.double(21))").unwrap();
13456
13457 let source = std::fs::read_to_string(&main_file).unwrap();
13458 let program = tl_parser::parse(&source).unwrap();
13459 let proto = crate::compiler::compile(&program).unwrap();
13460
13461 let mut vm = Vm::new();
13462 vm.file_path = Some(main_file.to_string_lossy().to_string());
13463 vm.package_roots.insert("utils".into(), pkg_dir);
13464 vm.execute(&proto).unwrap();
13465
13466 assert_eq!(vm.output, vec!["42"]);
13467 }
13468
13469 #[test]
13470 fn test_vm_package_underscore_to_hyphen() {
13471 let tmp = tempfile::tempdir().unwrap();
13472 let pkg_dir = tmp.path().join("my-pkg");
13473 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13474 std::fs::write(pkg_dir.join("src/lib.tl"), "pub fn val() { print(99) }").unwrap();
13475 std::fs::write(
13476 pkg_dir.join("tl.toml"),
13477 "[project]\nname = \"my-pkg\"\nversion = \"1.0.0\"\n",
13478 )
13479 .unwrap();
13480
13481 let main_file = tmp.path().join("main.tl");
13483 std::fs::write(&main_file, "use my_pkg\nval()").unwrap();
13484
13485 let source = std::fs::read_to_string(&main_file).unwrap();
13486 let program = tl_parser::parse(&source).unwrap();
13487 let proto = crate::compiler::compile(&program).unwrap();
13488
13489 let mut vm = Vm::new();
13490 vm.file_path = Some(main_file.to_string_lossy().to_string());
13491 vm.package_roots.insert("my-pkg".into(), pkg_dir);
13492 vm.execute(&proto).unwrap();
13493
13494 assert_eq!(vm.output, vec!["99"]);
13495 }
13496
13497 #[test]
13498 fn test_vm_local_module_priority_over_package() {
13499 let tmp = tempfile::tempdir().unwrap();
13501
13502 std::fs::write(
13504 tmp.path().join("mymod.tl"),
13505 "pub fn val() { print(\"local\") }",
13506 )
13507 .unwrap();
13508
13509 let pkg_dir = tmp.path().join("pkg_mymod");
13511 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13512 std::fs::write(
13513 pkg_dir.join("src/lib.tl"),
13514 "pub fn val() { print(\"package\") }",
13515 )
13516 .unwrap();
13517
13518 let main_file = tmp.path().join("main.tl");
13520 std::fs::write(&main_file, "use mymod\nval()").unwrap();
13521
13522 let source = std::fs::read_to_string(&main_file).unwrap();
13523 let program = tl_parser::parse(&source).unwrap();
13524 let proto = crate::compiler::compile(&program).unwrap();
13525
13526 let mut vm = Vm::new();
13527 vm.file_path = Some(main_file.to_string_lossy().to_string());
13528 vm.package_roots.insert("mymod".into(), pkg_dir);
13529 vm.execute(&proto).unwrap();
13530
13531 assert_eq!(vm.output, vec!["local"]);
13533 }
13534
13535 #[test]
13536 fn test_vm_package_missing_error() {
13537 let tmp = tempfile::tempdir().unwrap();
13538 let main_file = tmp.path().join("main.tl");
13539 std::fs::write(&main_file, "use nonexistent\nnonexistent.foo()").unwrap();
13540
13541 let source = std::fs::read_to_string(&main_file).unwrap();
13542 let program = tl_parser::parse(&source).unwrap();
13543 let proto = crate::compiler::compile(&program).unwrap();
13544
13545 let mut vm = Vm::new();
13546 vm.file_path = Some(main_file.to_string_lossy().to_string());
13547 let result = vm.execute(&proto);
13548
13549 assert!(result.is_err());
13550 let err = format!("{:?}", result.unwrap_err());
13551 assert!(err.contains("Module not found"));
13552 }
13553
13554 #[test]
13555 #[cfg(feature = "native")]
13556 fn test_resolve_package_file_entry_points() {
13557 let tmp = tempfile::tempdir().unwrap();
13558
13559 std::fs::create_dir_all(tmp.path().join("src")).unwrap();
13561 std::fs::write(tmp.path().join("src/lib.tl"), "").unwrap();
13562 let result = resolve_package_file(tmp.path(), &[]);
13563 assert!(result.is_some());
13564 assert!(result.unwrap().contains("lib.tl"));
13565
13566 std::fs::write(tmp.path().join("src/math.tl"), "").unwrap();
13568 let result = resolve_package_file(tmp.path(), &["math"]);
13569 assert!(result.is_some());
13570 assert!(result.unwrap().contains("math.tl"));
13571 }
13572
13573 #[test]
13574 fn test_vm_package_propagates_to_sub_imports() {
13575 let tmp = tempfile::tempdir().unwrap();
13577
13578 let pkg_dir = tmp.path().join("helpers");
13580 std::fs::create_dir_all(pkg_dir.join("src")).unwrap();
13581 std::fs::write(
13582 pkg_dir.join("src/lib.tl"),
13583 "pub fn help() { print(\"helped\") }",
13584 )
13585 .unwrap();
13586 std::fs::write(
13587 pkg_dir.join("tl.toml"),
13588 "[project]\nname = \"helpers\"\nversion = \"1.0.0\"\n",
13589 )
13590 .unwrap();
13591
13592 std::fs::write(
13594 tmp.path().join("bridge.tl"),
13595 "use helpers\npub fn run() { help() }",
13596 )
13597 .unwrap();
13598
13599 let main_file = tmp.path().join("main.tl");
13601 std::fs::write(&main_file, "use bridge\nrun()").unwrap();
13602
13603 let source = std::fs::read_to_string(&main_file).unwrap();
13604 let program = tl_parser::parse(&source).unwrap();
13605 let proto = crate::compiler::compile(&program).unwrap();
13606
13607 let mut vm = Vm::new();
13608 vm.file_path = Some(main_file.to_string_lossy().to_string());
13609 vm.package_roots.insert("helpers".into(), pkg_dir);
13610 vm.execute(&proto).unwrap();
13611
13612 assert_eq!(vm.output, vec!["helped"]);
13613 }
13614
13615 #[test]
13618 fn test_block_body_closure_basic() {
13619 let output =
13620 run_output("let f = (x: int64) -> int64 { let y = x * 2\n y + 1 }\nprint(f(5))");
13621 assert_eq!(output, vec!["11"]);
13622 }
13623
13624 #[test]
13625 fn test_block_body_closure_captures_upvalue() {
13626 let output = run_output(
13627 "let offset = 10\nlet f = (x) -> int64 { let y = x + offset\n y }\nprint(f(5))",
13628 );
13629 assert_eq!(output, vec!["15"]);
13630 }
13631
13632 #[test]
13633 fn test_block_body_closure_as_hof_arg() {
13634 let output = run_output(
13635 "let nums = [1, 2, 3]\nlet result = map(nums, (x) -> int64 { let doubled = x * 2\n doubled + 1 })\nprint(result)",
13636 );
13637 assert_eq!(output, vec!["[3, 5, 7]"]);
13638 }
13639
13640 #[test]
13641 fn test_block_body_closure_multi_stmt() {
13642 let output = run_output(
13643 "let f = (a, b) -> int64 { let sum = a + b\n let product = a * b\n sum + product }\nprint(f(3, 4))",
13644 );
13645 assert_eq!(output, vec!["19"]);
13646 }
13647
13648 #[test]
13649 fn test_type_alias_noop() {
13650 let output = run_output(
13652 "type Mapper = fn(int64) -> int64\nlet f: Mapper = (x) => x * 2\nprint(f(5))",
13653 );
13654 assert_eq!(output, vec!["10"]);
13655 }
13656
13657 #[test]
13658 fn test_type_alias_in_function_sig() {
13659 let output = run_output(
13660 "type Mapper = fn(int64) -> int64\nfn apply(f: Mapper, x: int64) -> int64 { f(x) }\nprint(apply((x) => x + 10, 5))",
13661 );
13662 assert_eq!(output, vec!["15"]);
13663 }
13664
13665 #[test]
13666 fn test_shorthand_closure() {
13667 let output = run_output("let double = x => x * 2\nprint(double(5))");
13668 assert_eq!(output, vec!["10"]);
13669 }
13670
13671 #[test]
13672 fn test_shorthand_closure_in_map() {
13673 let output = run_output("let nums = [1, 2, 3]\nprint(map(nums, x => x * 2))");
13674 assert_eq!(output, vec!["[2, 4, 6]"]);
13675 }
13676
13677 #[test]
13678 fn test_iife() {
13679 let output = run_output("let r = ((x) => x * 2)(5)\nprint(r)");
13680 assert_eq!(output, vec!["10"]);
13681 }
13682
13683 #[test]
13684 fn test_hof_apply() {
13685 let output = run_output("fn apply(f, x) { f(x) }\nprint(apply((x) => x + 10, 5))");
13686 assert_eq!(output, vec!["15"]);
13687 }
13688
13689 #[test]
13690 fn test_closure_stored_in_list() {
13691 let output = run_output(
13692 "let fns = [(x) => x + 1, (x) => x * 2]\nprint(fns[0](5))\nprint(fns[1](5))",
13693 );
13694 assert_eq!(output, vec!["6", "10"]);
13695 }
13696
13697 #[test]
13698 fn test_block_body_closure_with_return() {
13699 let output = run_output(
13701 "let classify = (x) -> string { if x > 0 { return \"positive\" }\n \"non-positive\" }\nprint(classify(5))\nprint(classify(-1))",
13702 );
13703 assert_eq!(output, vec!["positive", "non-positive"]);
13704 }
13705
13706 #[test]
13707 fn test_shorthand_closure_in_filter() {
13708 let output = run_output(
13709 "let nums = [1, 2, 3, 4, 5, 6]\nlet evens = filter(nums, x => x % 2 == 0)\nprint(evens)",
13710 );
13711 assert_eq!(output, vec!["[2, 4, 6]"]);
13712 }
13713
13714 #[test]
13715 fn test_block_closure_with_multiple_returns() {
13716 let output = run_output(
13717 "let abs_val = (x) -> int64 { if x < 0 { return -x }\n x }\nprint(abs_val(-5))\nprint(abs_val(3))",
13718 );
13719 assert_eq!(output, vec!["5", "3"]);
13720 }
13721
13722 #[test]
13723 fn test_type_alias_with_block_closure() {
13724 let output = run_output(
13725 "type Transform = fn(int64) -> int64\nlet f: Transform = (x) -> int64 { let y = x * x\n y + 1 }\nprint(f(3))",
13726 );
13727 assert_eq!(output, vec!["10"]);
13728 }
13729
13730 #[test]
13731 fn test_closure_both_backends_expr() {
13732 let output = run_output("let f = (x) => x * 3 + 1\nprint(f(4))");
13734 assert_eq!(output, vec!["13"]);
13735 }
13736
13737 #[test]
13739 #[cfg(not(feature = "python"))]
13740 fn test_py_feature_disabled() {
13741 let result = run("py_import(\"math\")");
13742 assert!(result.is_err());
13743 let msg = format!("{}", result.unwrap_err());
13744 assert!(msg.contains("python") && msg.contains("feature"));
13745 }
13746
13747 #[test]
13748 #[cfg(feature = "python")]
13749 fn test_vm_py_import_and_eval() {
13750 pyo3::prepare_freethreaded_python();
13751 let output = run_output("let m = py_import(\"math\")\nlet pi = m.pi\nprint(pi)");
13752 assert_eq!(output.len(), 1);
13753 let pi: f64 = output[0].parse().unwrap();
13754 assert!((pi - std::f64::consts::PI).abs() < 1e-10);
13755 }
13756
13757 #[test]
13758 #[cfg(feature = "python")]
13759 fn test_vm_py_eval_arithmetic() {
13760 pyo3::prepare_freethreaded_python();
13761 let output = run_output("let x = py_eval(\"2 ** 10\")\nprint(x)");
13762 assert_eq!(output, vec!["1024"]);
13763 }
13764
13765 #[test]
13766 #[cfg(feature = "python")]
13767 fn test_vm_py_method_dispatch() {
13768 pyo3::prepare_freethreaded_python();
13769 let output = run_output("let m = py_import(\"math\")\nprint(m.sqrt(25.0))");
13770 assert_eq!(output, vec!["5.0"]);
13771 }
13772
13773 #[test]
13774 #[cfg(feature = "python")]
13775 fn test_vm_py_list_conversion() {
13776 pyo3::prepare_freethreaded_python();
13777 let output = run_output("let x = py_eval(\"[10, 20, 30]\")\nprint(x)");
13778 assert_eq!(output, vec!["[10, 20, 30]"]);
13779 }
13780
13781 #[test]
13782 #[cfg(feature = "python")]
13783 fn test_vm_py_none_conversion() {
13784 pyo3::prepare_freethreaded_python();
13785 let output = run_output("let x = py_eval(\"None\")\nprint(x)");
13786 assert_eq!(output, vec!["none"]);
13787 }
13788
13789 #[test]
13790 #[cfg(feature = "python")]
13791 fn test_vm_py_error_msg_quality() {
13792 pyo3::prepare_freethreaded_python();
13793 let result = run("py_import(\"nonexistent_xyz_module\")");
13794 assert!(result.is_err());
13795 let msg = format!("{}", result.unwrap_err());
13796 assert!(msg.contains("py_import") && msg.contains("nonexistent_xyz_module"));
13797 }
13798
13799 #[test]
13800 #[cfg(feature = "python")]
13801 fn test_vm_py_getattr_setattr() {
13802 pyo3::prepare_freethreaded_python();
13803 let output = run_output(
13804 "let t = py_import(\"types\")\nlet obj = py_call(py_getattr(t, \"SimpleNamespace\"))\npy_setattr(obj, \"val\", 99)\nprint(py_getattr(obj, \"val\"))",
13805 );
13806 assert_eq!(output, vec!["99"]);
13807 }
13808
13809 #[test]
13810 #[cfg(feature = "python")]
13811 fn test_vm_py_callable_round_trip() {
13812 pyo3::prepare_freethreaded_python();
13813 let output = run_output(
13814 "let m = py_import(\"math\")\nlet f = py_getattr(m, \"floor\")\nprint(py_call(f, 3.7))",
13815 );
13816 assert_eq!(output, vec!["3"]);
13817 }
13818
13819 #[test]
13822 fn test_vm_schema_register_and_get() {
13823 let source = r#"let fields = map_from("id", "int64", "name", "string")
13824schema_register("User", 1, fields)
13825let result = schema_get("User", 1)
13826print(len(result))"#;
13827 let output = run_output(source);
13828 assert_eq!(output, vec!["2"]);
13829 }
13830
13831 #[test]
13832 fn test_vm_schema_latest() {
13833 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13834schema_register("User", 2, map_from("id", "int64", "name", "string"))
13835let latest = schema_latest("User")
13836print(latest)"#;
13837 let output = run_output(source);
13838 assert_eq!(output, vec!["2"]);
13839 }
13840
13841 #[test]
13842 fn test_vm_schema_history() {
13843 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13844schema_register("User", 2, map_from("id", "int64", "name", "string"))
13845let hist = schema_history("User")
13846print(len(hist))"#;
13847 let output = run_output(source);
13848 assert_eq!(output, vec!["2"]);
13849 }
13850
13851 #[test]
13852 fn test_vm_schema_check_backward_compat() {
13853 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13854schema_register("User", 2, map_from("id", "int64", "name", "string"))
13855let issues = schema_check("User", 1, 2, "backward")
13856print(len(issues))"#;
13857 let output = run_output(source);
13858 assert_eq!(output, vec!["0"]);
13859 }
13860
13861 #[test]
13862 fn test_vm_schema_diff() {
13863 let source = r#"schema_register("User", 1, map_from("id", "int64"))
13864schema_register("User", 2, map_from("id", "int64", "name", "string"))
13865let diffs = schema_diff("User", 1, 2)
13866print(len(diffs))"#;
13867 let output = run_output(source);
13868 assert_eq!(output, vec!["1"]);
13869 }
13870
13871 #[test]
13872 fn test_vm_schema_versions() {
13873 let source = r#"schema_register("T", 1, map_from("id", "int64"))
13874schema_register("T", 3, map_from("id", "int64"))
13875schema_register("T", 2, map_from("id", "int64"))
13876let vers = schema_versions("T")
13877print(len(vers))"#;
13878 let output = run_output(source);
13879 assert_eq!(output, vec!["3"]);
13880 }
13881
13882 #[test]
13883 fn test_vm_schema_fields() {
13884 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13885let fields = schema_fields("User", 1)
13886print(len(fields))"#;
13887 let output = run_output(source);
13888 assert_eq!(output, vec!["2"]);
13889 }
13890
13891 #[test]
13892 fn test_vm_compile_versioned_schema() {
13893 let source = "/// @version 1\nschema User { id: int64, name: string }\nprint(User)";
13894 let output = run_output(source);
13895 assert!(output[0].contains("__schema__:User:v1:"));
13896 }
13897
13898 #[test]
13899 fn test_vm_compile_migrate() {
13900 let source = "migrate User from 1 to 2 { add_column(email: string) }\nprint(\"ok\")";
13901 let output = run_output(source);
13902 assert_eq!(output, vec!["ok"]);
13903 }
13904
13905 #[test]
13906 fn test_vm_schema_check_backward_compat_fails() {
13907 let source = r#"schema_register("User", 1, map_from("id", "int64", "name", "string"))
13908schema_register("User", 2, map_from("id", "int64"))
13909let issues = schema_check("User", 1, 2, "backward")
13910print(len(issues))"#;
13911 let output = run_output(source);
13912 assert_eq!(output, vec!["1"]);
13913 }
13914
13915 #[test]
13918 fn test_vm_decimal_literal_and_arithmetic() {
13919 let output = run_output("let a = 10.5d\nlet b = 2.5d\nprint(a + b)\nprint(a * b)");
13920 assert_eq!(output, vec!["13.0", "26.25"]);
13921 }
13922
13923 #[test]
13924 fn test_vm_decimal_div_by_zero() {
13925 let source = "let a = 1.0d\nlet b = 0.0d\nlet c = a / b";
13926 let program = tl_parser::parse(source).unwrap();
13927 let proto = crate::compile(&program).unwrap();
13928 let mut vm = Vm::new();
13929 let result = vm.execute(&proto);
13930 assert!(result.is_err());
13931 }
13932
13933 #[test]
13934 fn test_vm_decimal_comparison_ops() {
13935 let output =
13936 run_output("let a = 1.0d\nlet b = 2.0d\nprint(a < b)\nprint(a >= b)\nprint(a == a)");
13937 assert_eq!(output, vec!["true", "false", "true"]);
13938 }
13939
13940 #[test]
13943 fn test_vm_secret_vault_crud() {
13944 let output = run_output(
13945 "secret_set(\"key\", \"value\")\nlet s = secret_get(\"key\")\nprint(s)\nsecret_delete(\"key\")\nlet s2 = secret_get(\"key\")\nprint(type_of(s2))",
13946 );
13947 assert_eq!(output, vec!["***", "none"]);
13948 }
13949
13950 #[test]
13951 fn test_vm_mask_email_basic() {
13952 let output = run_output("print(mask_email(\"alice@domain.com\"))");
13953 assert_eq!(output, vec!["a***@domain.com"]);
13954 }
13955
13956 #[test]
13957 fn test_vm_mask_phone_basic() {
13958 let output = run_output("print(mask_phone(\"123-456-7890\"))");
13959 assert_eq!(output, vec!["***-***-7890"]);
13960 }
13961
13962 #[test]
13963 fn test_vm_mask_cc_basic() {
13964 let output = run_output("print(mask_cc(\"4111222233334444\"))");
13965 assert_eq!(output, vec!["****-****-****-4444"]);
13966 }
13967
13968 #[test]
13969 fn test_vm_hash_produces_hex() {
13970 let output = run_output("let h = hash(\"test\", \"sha256\")\nprint(len(h))");
13971 assert_eq!(output, vec!["64"]);
13972 }
13973
13974 #[test]
13975 fn test_vm_redact_modes() {
13976 let output =
13977 run_output("print(redact(\"hello\", \"full\"))\nprint(redact(\"hello\", \"partial\"))");
13978 assert_eq!(output, vec!["***", "h***o"]);
13979 }
13980
13981 #[test]
13982 fn test_vm_security_policy_sandbox() {
13983 let source = "print(check_permission(\"network\"))\nprint(check_permission(\"file_read\"))";
13984 let program = tl_parser::parse(source).unwrap();
13985 let proto = crate::compile(&program).unwrap();
13986 let mut vm = Vm::new();
13987 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
13988 vm.execute(&proto).unwrap();
13989 assert_eq!(vm.output, vec!["false", "true"]);
13990 }
13991
13992 #[cfg(feature = "async-runtime")]
13995 #[test]
13996 fn test_vm_async_read_write_file() {
13997 let dir = tempfile::tempdir().unwrap();
13998 let path = dir.path().join("async_test.txt");
13999 let path_str = path.to_str().unwrap().replace('\\', "/");
14000 let source = format!(
14001 r#"let wt = async_write_file("{path_str}", "async hello")
14002let wr = await(wt)
14003let rt = async_read_file("{path_str}")
14004let content = await(rt)
14005print(content)"#
14006 );
14007 let output = run_output(&source);
14008 assert_eq!(output, vec!["async hello"]);
14009 }
14010
14011 #[cfg(feature = "async-runtime")]
14012 #[test]
14013 fn test_vm_async_sleep() {
14014 let source = r#"
14015let t = async_sleep(10)
14016let r = await(t)
14017print(r)
14018"#;
14019 let output = run_output(source);
14020 assert_eq!(output, vec!["none"]);
14021 }
14022
14023 #[cfg(feature = "async-runtime")]
14024 #[test]
14025 fn test_vm_select_first_wins() {
14026 let source = r#"
14028let fast = async_sleep(10)
14029let slow = async_sleep(5000)
14030let winner = select(fast, slow)
14031let result = await(winner)
14032print(result)
14033"#;
14034 let output = run_output(source);
14035 assert_eq!(output, vec!["none"]);
14036 }
14037
14038 #[cfg(feature = "async-runtime")]
14039 #[test]
14040 fn test_vm_race_all() {
14041 let source = r#"
14042let t1 = async_sleep(10)
14043let t2 = async_sleep(5000)
14044let winner = race_all([t1, t2])
14045let result = await(winner)
14046print(result)
14047"#;
14048 let output = run_output(source);
14049 assert_eq!(output, vec!["none"]);
14050 }
14051
14052 #[cfg(feature = "async-runtime")]
14053 #[test]
14054 fn test_vm_async_map() {
14055 let source = r#"
14056let items = [1, 2, 3]
14057let t = async_map(items, (x) => x * 10)
14058let result = await(t)
14059print(result)
14060"#;
14061 let output = run_output(source);
14062 assert_eq!(output, vec!["[10, 20, 30]"]);
14063 }
14064
14065 #[cfg(feature = "async-runtime")]
14066 #[test]
14067 fn test_vm_async_filter() {
14068 let source = r#"
14069let items = [1, 2, 3, 4, 5]
14070let t = async_filter(items, (x) => x > 3)
14071let result = await(t)
14072print(result)
14073"#;
14074 let output = run_output(source);
14075 assert_eq!(output, vec!["[4, 5]"]);
14076 }
14077
14078 #[cfg(feature = "async-runtime")]
14079 #[test]
14080 fn test_vm_async_write_file_returns_none() {
14081 let dir = tempfile::tempdir().unwrap();
14082 let path = dir.path().join("write_test.txt");
14083 let path_str = path.to_str().unwrap().replace('\\', "/");
14084 let source = format!(
14085 r#"let t = async_write_file("{path_str}", "test data")
14086let r = await(t)
14087print(r)"#
14088 );
14089 let output = run_output(&source);
14090 assert_eq!(output, vec!["none"]);
14091 }
14092
14093 #[cfg(feature = "async-runtime")]
14094 #[test]
14095 fn test_vm_async_security_policy_blocks_write() {
14096 let source = r#"let t = async_write_file("/tmp/blocked.txt", "data")"#;
14097 let program = tl_parser::parse(source).unwrap();
14098 let proto = crate::compile(&program).unwrap();
14099 let mut vm = Vm::new();
14100 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14101 let result = vm.execute(&proto);
14102 assert!(result.is_err());
14103 let err = format!("{}", result.unwrap_err());
14104 assert!(
14105 err.contains("file_write not allowed"),
14106 "Expected security error, got: {err}"
14107 );
14108 }
14109
14110 #[cfg(feature = "async-runtime")]
14111 #[test]
14112 fn test_vm_async_security_policy_allows_read() {
14113 let dir = tempfile::tempdir().unwrap();
14115 let path = dir.path().join("readable.txt");
14116 std::fs::write(&path, "safe content").unwrap();
14117 let path_str = path.to_str().unwrap().replace('\\', "/");
14118 let source = format!(
14119 r#"let t = async_read_file("{path_str}")
14120let r = await(t)
14121print(r)"#
14122 );
14123 let program = tl_parser::parse(&source).unwrap();
14124 let proto = crate::compile(&program).unwrap();
14125 let mut vm = Vm::new();
14126 vm.security_policy = Some(crate::security::SecurityPolicy::sandbox());
14127 vm.execute(&proto).unwrap();
14128 assert_eq!(vm.output, vec!["safe content"]);
14129 }
14130
14131 #[cfg(feature = "async-runtime")]
14132 #[test]
14133 fn test_vm_async_map_empty_list() {
14134 let source = r#"
14135let t = async_map([], (x) => x * 2)
14136let result = await(t)
14137print(result)
14138"#;
14139 let output = run_output(source);
14140 assert_eq!(output, vec!["[]"]);
14141 }
14142
14143 #[cfg(feature = "async-runtime")]
14144 #[test]
14145 fn test_vm_async_filter_none_match() {
14146 let source = r#"
14147let t = async_filter([1, 2, 3], (x) => x > 100)
14148let result = await(t)
14149print(result)
14150"#;
14151 let output = run_output(source);
14152 assert_eq!(output, vec!["[]"]);
14153 }
14154
14155 #[test]
14158 fn test_vm_closure_returned_from_function() {
14159 let output = run_output(
14160 r#"
14161fn make_adder(n) {
14162 return (x) => x + n
14163}
14164let add5 = make_adder(5)
14165print(add5(3))
14166print(add5(10))
14167"#,
14168 );
14169 assert_eq!(output, vec!["8", "15"]);
14170 }
14171
14172 #[test]
14173 fn test_vm_closure_factory_multiple_calls() {
14174 let output = run_output(
14175 r#"
14176fn make_adder(n) {
14177 return (x) => x + n
14178}
14179let add2 = make_adder(2)
14180let add10 = make_adder(10)
14181print(add2(5))
14182print(add10(5))
14183print(add2(1))
14184"#,
14185 );
14186 assert_eq!(output, vec!["7", "15", "3"]);
14187 }
14188
14189 #[test]
14190 fn test_vm_closure_returned_in_list() {
14191 let output = run_output(
14192 r#"
14193fn make_ops(n) {
14194 let add = (x) => x + n
14195 let mul = (x) => x * n
14196 return [add, mul]
14197}
14198let ops = make_ops(3)
14199print(ops[0](10))
14200print(ops[1](10))
14201"#,
14202 );
14203 assert_eq!(output, vec!["13", "30"]);
14204 }
14205
14206 #[test]
14207 fn test_vm_nested_closure_return() {
14208 let output = run_output(
14209 r#"
14210fn outer(a) {
14211 fn inner(b) {
14212 return (x) => x + a + b
14213 }
14214 return inner(10)
14215}
14216let f = outer(5)
14217print(f(1))
14218"#,
14219 );
14220 assert_eq!(output, vec!["16"]);
14221 }
14222
14223 #[test]
14224 fn test_vm_multiple_closures_same_local() {
14225 let output = run_output(
14226 r#"
14227fn make_pair(n) {
14228 let inc = (x) => x + n
14229 let dec = (x) => x - n
14230 return [inc, dec]
14231}
14232let pair = make_pair(7)
14233print(pair[0](10))
14234print(pair[1](10))
14235"#,
14236 );
14237 assert_eq!(output, vec!["17", "3"]);
14238 }
14239
14240 #[test]
14241 fn test_vm_closure_captures_multiple_locals() {
14242 let output = run_output(
14243 r#"
14244fn make_greeter(greeting, name) {
14245 let sep = " "
14246 return () => greeting + sep + name
14247}
14248let hi = make_greeter("Hello", "World")
14249let bye = make_greeter("Goodbye", "Alice")
14250print(hi())
14251print(bye())
14252"#,
14253 );
14254 assert_eq!(output, vec!["Hello World", "Goodbye Alice"]);
14255 }
14256
14257 #[test]
14260 fn test_vm_throw_catch_preserves_enum() {
14261 let output = run_output(
14262 r#"
14263enum Color { Red, Green(x) }
14264try {
14265 throw Color::Green(42)
14266} catch e {
14267 match e {
14268 Color::Green(x) => print(x),
14269 _ => print("no match"),
14270 }
14271}
14272"#,
14273 );
14274 assert_eq!(output, vec!["42"]);
14275 }
14276
14277 #[test]
14278 fn test_vm_throw_catch_string_compat() {
14279 let output = run_output(
14280 r#"
14281try {
14282 throw "hello error"
14283} catch e {
14284 print(e)
14285}
14286"#,
14287 );
14288 assert_eq!(output, vec!["hello error"]);
14289 }
14290
14291 #[test]
14292 fn test_vm_runtime_error_still_string() {
14293 let output = run_output(
14294 r#"
14295try {
14296 let x = 1 / 0
14297} catch e {
14298 print(type_of(e))
14299}
14300"#,
14301 );
14302 assert_eq!(output, vec!["string"]);
14303 }
14304
14305 #[test]
14306 fn test_vm_data_error_construct_and_throw() {
14307 let output = run_output(
14308 r#"
14309try {
14310 throw DataError::ParseError("bad format", "file.csv")
14311} catch e {
14312 print(match e { DataError::ParseError(msg, _) => msg, _ => "no match" })
14313 print(match e { DataError::ParseError(_, src) => src, _ => "no match" })
14314}
14315"#,
14316 );
14317 assert_eq!(output, vec!["bad format", "file.csv"]);
14318 }
14319
14320 #[test]
14321 fn test_vm_network_error_construct() {
14322 let output = run_output(
14323 r#"
14324let err = NetworkError::TimeoutError("timed out")
14325match err {
14326 NetworkError::TimeoutError(msg) => print(msg),
14327 _ => print("no match"),
14328}
14329"#,
14330 );
14331 assert_eq!(output, vec!["timed out"]);
14332 }
14333
14334 #[test]
14335 fn test_vm_connector_error_construct() {
14336 let output = run_output(
14337 r#"
14338let err = ConnectorError::AuthError("invalid creds", "postgres")
14339print(match err { ConnectorError::AuthError(msg, _) => msg, _ => "no match" })
14340print(match err { ConnectorError::AuthError(_, conn) => conn, _ => "no match" })
14341"#,
14342 );
14343 assert_eq!(output, vec!["invalid creds", "postgres"]);
14344 }
14345
14346 #[test]
14347 fn test_vm_is_error_builtin() {
14348 let output = run_output(
14349 r#"
14350let e1 = DataError::NotFound("users")
14351let e2 = NetworkError::TimeoutError("slow")
14352let e3 = ConnectorError::ConfigError("bad", "redis")
14353let e4 = "not an error"
14354print(is_error(e1))
14355print(is_error(e2))
14356print(is_error(e3))
14357print(is_error(e4))
14358"#,
14359 );
14360 assert_eq!(output, vec!["true", "true", "true", "false"]);
14361 }
14362
14363 #[test]
14364 fn test_vm_error_type_builtin() {
14365 let output = run_output(
14366 r#"
14367let e1 = DataError::ParseError("bad", "x.csv")
14368let e2 = NetworkError::HttpError("fail", "url")
14369let e3 = "not an error"
14370print(error_type(e1))
14371print(error_type(e2))
14372print(error_type(e3))
14373"#,
14374 );
14375 assert_eq!(output, vec!["DataError", "NetworkError", "none"]);
14376 }
14377
14378 #[test]
14379 fn test_vm_match_error_variants() {
14380 let output = run_output(
14381 r#"
14382fn handle(err) {
14383 print(match err {
14384 DataError::ParseError(msg, _) => "parse: " + msg,
14385 DataError::SchemaError(msg, _, _) => "schema: " + msg,
14386 DataError::ValidationError(_, field) => "validation: " + field,
14387 DataError::NotFound(name) => "not found: " + name,
14388 _ => "unknown"
14389 })
14390}
14391handle(DataError::ParseError("bad csv", "data.csv"))
14392handle(DataError::NotFound("users_table"))
14393handle(DataError::SchemaError("mismatch", "int", "string"))
14394handle(DataError::ValidationError("invalid", "email"))
14395"#,
14396 );
14397 assert_eq!(
14398 output,
14399 vec![
14400 "parse: bad csv",
14401 "not found: users_table",
14402 "schema: mismatch",
14403 "validation: email",
14404 ]
14405 );
14406 }
14407
14408 #[test]
14409 fn test_vm_rethrow_structured_error() {
14410 let output = run_output(
14411 r#"
14412try {
14413 try {
14414 throw DataError::NotFound("config")
14415 } catch e {
14416 throw e
14417 }
14418} catch outer {
14419 match outer {
14420 DataError::NotFound(name) => print("caught: " + name),
14421 _ => print("wrong type"),
14422 }
14423}
14424"#,
14425 );
14426 assert_eq!(output, vec!["caught: config"]);
14427 }
14428
14429 #[test]
14432 fn test_vm_pipe_moves_value() {
14433 let result = run(r#"
14435fn identity(v) { v }
14436let x = [1, 2, 3]
14437x |> identity()
14438print(x)
14439"#);
14440 assert!(result.is_err());
14441 let err = result.unwrap_err().to_string();
14442 assert!(err.contains("moved"), "Error should mention 'moved': {err}");
14443 }
14444
14445 #[test]
14446 fn test_vm_clone_before_pipe() {
14447 let output = run_output(
14449 r#"
14450fn identity(v) { v }
14451let x = [1, 2, 3]
14452x.clone() |> identity()
14453print(x)
14454"#,
14455 );
14456 assert_eq!(output, vec!["[1, 2, 3]"]);
14457 }
14458
14459 #[test]
14460 fn test_vm_clone_list_deep() {
14461 let output = run_output(
14463 r#"
14464let original = [1, 2, 3]
14465let copy = original.clone()
14466copy[0] = 99
14467print(original)
14468print(copy)
14469"#,
14470 );
14471 assert_eq!(output, vec!["[1, 2, 3]", "[99, 2, 3]"]);
14472 }
14473
14474 #[test]
14475 fn test_vm_clone_map() {
14476 let output = run_output(
14477 r#"
14478let m = map_from("a", 1, "b", 2)
14479let m2 = m.clone()
14480m2["a"] = 99
14481print(m)
14482print(m2)
14483"#,
14484 );
14485 assert_eq!(output, vec!["{a: 1, b: 2}", "{a: 99, b: 2}"]);
14486 }
14487
14488 #[test]
14489 fn test_vm_clone_struct() {
14490 let output = run_output(
14491 r#"
14492struct Point { x: int64, y: int64 }
14493let p = Point { x: 1, y: 2 }
14494let p2 = p.clone()
14495print(p)
14496print(p2)
14497"#,
14498 );
14499 assert_eq!(output, vec!["Point { x: 1, y: 2 }", "Point { x: 1, y: 2 }"]);
14500 }
14501
14502 #[test]
14503 fn test_vm_ref_read_only() {
14504 let result = run(r#"
14506let x = [1, 2, 3]
14507let r = &x
14508r[0] = 99
14509"#);
14510 assert!(result.is_err());
14511 let err = result.unwrap_err().to_string();
14512 assert!(
14513 err.contains("Cannot mutate a borrowed reference"),
14514 "Error should mention reference: {err}"
14515 );
14516 }
14517
14518 #[test]
14519 fn test_vm_ref_transparent_read() {
14520 let output = run_output(
14522 r#"
14523let x = [1, 2, 3]
14524let r = &x
14525print(len(r))
14526"#,
14527 );
14528 assert_eq!(output, vec!["3"]);
14529 }
14530
14531 #[test]
14532 fn test_vm_parallel_for_basic() {
14533 let output = run_output(
14535 r#"
14536let items = [10, 20, 30]
14537parallel for item in items {
14538 print(item)
14539}
14540"#,
14541 );
14542 assert_eq!(output, vec!["10", "20", "30"]);
14543 }
14544
14545 #[test]
14546 fn test_vm_moved_value_clear_error() {
14547 let result = run(r#"
14549fn f(x) { x }
14550let data = "hello"
14551data |> f()
14552print(data)
14553"#);
14554 assert!(result.is_err());
14555 let err = result.unwrap_err().to_string();
14556 assert!(
14557 err.contains("clone()"),
14558 "Error should suggest .clone(): {err}"
14559 );
14560 }
14561
14562 #[test]
14563 fn test_vm_reassign_after_move() {
14564 let output = run_output(
14566 r#"
14567fn f(x) { x }
14568let x = 1
14569x |> f()
14570let x = 2
14571print(x)
14572"#,
14573 );
14574 assert_eq!(output, vec!["2"]);
14575 }
14576
14577 #[test]
14578 fn test_vm_pipe_chain_move() {
14579 let output = run_output(
14581 r#"
14582fn double(x) { x * 2 }
14583fn add_one(x) { x + 1 }
14584let result = 5 |> double() |> add_one()
14585print(result)
14586"#,
14587 );
14588 assert_eq!(output, vec!["11"]);
14589 }
14590
14591 #[test]
14592 fn test_vm_string_clone() {
14593 let output = run_output(
14595 r#"
14596let s = "hello"
14597let s2 = s.clone()
14598print(s)
14599print(s2)
14600"#,
14601 );
14602 assert_eq!(output, vec!["hello", "hello"]);
14603 }
14604
14605 #[test]
14606 fn test_vm_ref_method_dispatch() {
14607 let output = run_output(
14609 r#"
14610let s = "hello world"
14611let r = &s
14612print(r.len())
14613"#,
14614 );
14615 assert_eq!(output, vec!["11"]);
14616 }
14617
14618 #[test]
14619 fn test_vm_ref_member_access() {
14620 let output = run_output(
14622 r#"
14623struct Point { x: int64, y: int64 }
14624let p = Point { x: 10, y: 20 }
14625let r = &p
14626print(r.x)
14627"#,
14628 );
14629 assert_eq!(output, vec!["10"]);
14630 }
14631
14632 #[test]
14633 fn test_vm_ref_set_member_blocked() {
14634 let result = run(r#"
14636struct Point { x: int64, y: int64 }
14637let p = Point { x: 10, y: 20 }
14638let r = &p
14639r.x = 99
14640"#);
14641 assert!(result.is_err());
14642 let err = result.unwrap_err().to_string();
14643 assert!(
14644 err.contains("Cannot mutate a borrowed reference"),
14645 "Error: {err}"
14646 );
14647 }
14648
14649 #[test]
14652 fn test_ir_filter_merge_chain() {
14653 let dir = tempfile::tempdir().unwrap();
14655 let csv = dir.path().join("data.csv");
14656 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\nCharlie,35\n").unwrap();
14657 let src = format!(
14658 r#"let t = read_csv("{}")
14659let r = t |> filter(age > 25) |> filter(age < 40) |> collect()
14660print(r)"#,
14661 csv.to_str().unwrap()
14662 );
14663 let output = run_output(&src);
14664 assert!(
14666 output[0].contains("Alice"),
14667 "Output should contain Alice: {}",
14668 output[0]
14669 );
14670 assert!(
14671 output[0].contains("Charlie"),
14672 "Output should contain Charlie: {}",
14673 output[0]
14674 );
14675 assert!(
14676 !output[0].contains("Bob"),
14677 "Output should not contain Bob: {}",
14678 output[0]
14679 );
14680 }
14681
14682 #[test]
14683 fn test_ir_predicate_pushdown_through_select() {
14684 let dir = tempfile::tempdir().unwrap();
14686 let csv = dir.path().join("data.csv");
14687 std::fs::write(
14688 &csv,
14689 "name,age,city\nAlice,30,NYC\nBob,20,LA\nCharlie,35,NYC\n",
14690 )
14691 .unwrap();
14692 let src = format!(
14693 r#"let t = read_csv("{}")
14694let r = t |> select(name, age) |> filter(age > 25) |> collect()
14695print(r)"#,
14696 csv.to_str().unwrap()
14697 );
14698 let output = run_output(&src);
14699 assert!(output[0].contains("Alice"), "Output should contain Alice");
14700 assert!(
14701 output[0].contains("Charlie"),
14702 "Output should contain Charlie"
14703 );
14704 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14705 }
14706
14707 #[test]
14708 fn test_ir_sort_filter_pushdown() {
14709 let dir = tempfile::tempdir().unwrap();
14711 let csv = dir.path().join("data.csv");
14712 std::fs::write(&csv, "name,score\nAlice,90\nBob,50\nCharlie,75\n").unwrap();
14713 let src = format!(
14714 r#"let t = read_csv("{}")
14715let r = t |> sort(score, "desc") |> filter(score > 60) |> collect()
14716print(r)"#,
14717 csv.to_str().unwrap()
14718 );
14719 let output = run_output(&src);
14720 assert!(output[0].contains("Alice"), "Output should contain Alice");
14721 assert!(
14722 output[0].contains("Charlie"),
14723 "Output should contain Charlie"
14724 );
14725 assert!(!output[0].contains("Bob"), "Output should not contain Bob");
14726 }
14727
14728 #[test]
14729 fn test_ir_multi_operation_chain() {
14730 let dir = tempfile::tempdir().unwrap();
14732 let csv = dir.path().join("data.csv");
14733 std::fs::write(
14734 &csv,
14735 "name,age,dept\nAlice,30,Eng\nBob,20,Sales\nCharlie,35,Eng\nDiana,28,Sales\n",
14736 )
14737 .unwrap();
14738 let src = format!(
14739 r#"let t = read_csv("{}")
14740let r = t |> filter(age > 22) |> select(name, age) |> sort(age, "desc") |> limit(2) |> collect()
14741print(r)"#,
14742 csv.to_str().unwrap()
14743 );
14744 let output = run_output(&src);
14745 assert!(output[0].contains("Charlie"), "Output: {}", output[0]);
14747 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14748 }
14749
14750 #[test]
14751 fn test_ir_pipe_move_semantics_preserved() {
14752 let dir = tempfile::tempdir().unwrap();
14754 let csv = dir.path().join("data.csv");
14755 std::fs::write(&csv, "name,age\nAlice,30\n").unwrap();
14756 let src = format!(
14757 r#"let t = read_csv("{}")
14758let r = t |> filter(age > 0) |> collect()
14759print(t)"#,
14760 csv.to_str().unwrap()
14761 );
14762 let result = run(&src);
14763 assert!(result.is_err(), "Should error on use-after-move");
14764 }
14765
14766 #[test]
14767 fn test_ir_non_table_op_fallback() {
14768 let output = run_output(
14770 r#"
14771fn double(x) { x * 2 }
14772let result = 5 |> double()
14773print(result)
14774"#,
14775 );
14776 assert_eq!(output, vec!["10"]);
14777 }
14778
14779 #[test]
14780 fn test_ir_mixed_pipe_fallback() {
14781 let output = run_output(
14783 r#"
14784let result = [3, 1, 2] |> len()
14785print(result)
14786"#,
14787 );
14788 assert_eq!(output, vec!["3"]);
14789 }
14790
14791 #[test]
14792 fn test_ir_single_filter_roundtrip() {
14793 let dir = tempfile::tempdir().unwrap();
14795 let csv = dir.path().join("data.csv");
14796 std::fs::write(&csv, "name,age\nAlice,30\nBob,20\n").unwrap();
14797 let src = format!(
14798 r#"let t = read_csv("{}")
14799let r = t |> filter(age > 25) |> collect()
14800print(r)"#,
14801 csv.to_str().unwrap()
14802 );
14803 let output = run_output(&src);
14804 assert!(output[0].contains("Alice"), "Output: {}", output[0]);
14805 assert!(!output[0].contains("Bob"), "Output: {}", output[0]);
14806 }
14807
14808 #[test]
14811 fn test_vm_agent_definition() {
14812 let output = run_output(
14813 r#"
14814fn search(query) { "found: " + query }
14815agent bot {
14816 model: "gpt-4o",
14817 system: "You are helpful.",
14818 tools {
14819 search: {
14820 description: "Search the web",
14821 parameters: {}
14822 }
14823 },
14824 max_turns: 5
14825}
14826print(type_of(bot))
14827print(bot)
14828"#,
14829 );
14830 assert_eq!(output, vec!["agent", "<agent bot>"]);
14831 }
14832
14833 #[test]
14834 fn test_vm_agent_minimal() {
14835 let output = run_output(
14836 r#"
14837agent minimal_bot {
14838 model: "claude-sonnet-4-20250514"
14839}
14840print(type_of(minimal_bot))
14841"#,
14842 );
14843 assert_eq!(output, vec!["agent"]);
14844 }
14845
14846 #[test]
14847 fn test_vm_agent_with_base_url() {
14848 let output = run_output(
14849 r#"
14850agent local_bot {
14851 model: "llama3",
14852 base_url: "http://localhost:11434/v1",
14853 max_turns: 3
14854}
14855print(local_bot)
14856"#,
14857 );
14858 assert_eq!(output, vec!["<agent local_bot>"]);
14859 }
14860
14861 #[test]
14862 fn test_vm_agent_multiple_tools() {
14863 let output = run_output(
14864 r#"
14865fn search(query) { "result" }
14866fn weather(city) { "sunny" }
14867agent helper {
14868 model: "gpt-4o",
14869 tools {
14870 search: { description: "Search", parameters: {} },
14871 weather: { description: "Get weather", parameters: {} }
14872 }
14873}
14874print(type_of(helper))
14875"#,
14876 );
14877 assert_eq!(output, vec!["agent"]);
14878 }
14879
14880 #[test]
14881 fn test_vm_agent_lifecycle_hooks_stored() {
14882 let output = run_output(
14883 r#"
14884fn search(q) { "result" }
14885agent bot {
14886 model: "gpt-4o",
14887 tools {
14888 search: { description: "Search", parameters: {} }
14889 },
14890 on_tool_call {
14891 println("tool: " + tool_name)
14892 }
14893 on_complete {
14894 println("done")
14895 }
14896}
14897print(type_of(bot))
14898print(type_of(__agent_bot_on_tool_call__))
14899print(type_of(__agent_bot_on_complete__))
14900"#,
14901 );
14902 assert_eq!(output, vec!["agent", "function", "function"]);
14903 }
14904
14905 #[test]
14906 fn test_vm_agent_lifecycle_hook_callable() {
14907 let output = run_output(
14908 r#"
14909agent bot {
14910 model: "gpt-4o",
14911 on_tool_call {
14912 println("called: " + tool_name + " -> " + tool_result)
14913 }
14914 on_complete {
14915 println("completed")
14916 }
14917}
14918__agent_bot_on_tool_call__("search", "query", "found it")
14919__agent_bot_on_complete__("hello")
14920"#,
14921 );
14922 assert_eq!(output, vec!["called: search -> found it", "completed"]);
14923 }
14924}