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